2024 csp-j 复赛真题
# [CSP-J 2024] 扑克牌
# 题目描述
小 P 从同学小 Q 那儿借来一副 张牌的扑克牌。
本题中我们不考虑大小王,此时每张牌具有两个属性:花色和点数。花色共有 种:方片、草花、红桃和黑桃。点数共有 种,从小到大分别为 。注意:点数 在本题中记为 。
我们称一副扑克牌是完整的,当且仅当对于每一种花色和每一种点数,都恰好有一张牌具有对应的花色和点数。由此,一副完整的扑克牌恰好有 张牌。以下图片展示了一副完整的扑克牌里所有的 52 张牌。

小 P 借来的牌可能不是完整的,为此小 P 准备再向同学小 S 借若干张牌。可以认为小 S 每种牌都有无限张,因此小 P 可以任意选择借来的牌。小 P 想知道他至少得向小 S 借多少张牌,才能让从小 S 和小 Q 借来的牌中,可以选出 张牌构成一副完整的扑克牌。
为了方便你的输入,我们使用字符 代表方片,字符 代表草花,字符 代表红桃,字符 代表黑桃,这样每张牌可以通过一个长度为 的字符串表示,其中第一个字符表示这张牌的花色,第二个字符表示这张牌的点数,例如 表示草花 , 表示黑桃 (黑桃 10)。
# 输入格式
输入的第一行包含一个整数 表示牌数。
接下来 行:
每行包含一个长度为 的字符串描述一张牌,其中第一个字符描述其花色,第二个字符描述其点数。
# 输出格式
输出一行一个整数,表示最少还需要向小 S 借几张牌才能凑成一副完整的扑克牌。
# 样例 #1
# 样例输入 #1
1
SA
# 样例输出 #1
51
# 样例 #2
# 样例输入 #2
4
DQ
H3
DQ
DT
# 样例输出 #2
49
# 提示
【样例 1 解释】
这一副牌中包含一张黑桃 ,小 P 还需要借除了黑桃 以外的 51 张牌以构成一副完整的扑克牌。
【样例 2 解释】
这一副牌中包含两张方片 、一张方片 (方片 10)以及一张红桃 3,小 P 还需要借除了红桃 3、方片 和方片 以外的 张牌。
【样例 3 解释】
见选手目录下的 poker/poker3.in 与 poker/poker3.ans。
这一副扑克牌是完整的,故不需要再借任何牌。
该样例满足所有牌按照点数从小到大依次输入,点数相同时按照方片、草花、红桃、黑桃的顺序依次输入。
【数据范围】
对于所有测试数据,保证:,输入的 个字符串每个都代表一张合法的扑克牌,即字符串长度为 ,且第一个字符为 中的某个字符,第二个字符为 中的某个字符。
测试点编号 | 特殊性质 | |
---|---|---|
A | ||
A | ||
B | ||
无 |
特殊性质 A:保证输入的 张牌两两不同。
特殊性质 B:保证所有牌按照点数从小到大依次输入,点数相同时按照方片、草花、红桃、黑桃的顺序依次输入。
# 解析
我们注意到输入的牌字符串肯定是合法的,那么这题就变成了一个去重问题。
# 两种方法:
# 1. 使用 std::unique
std::unique
可以对一个有序的序列去重,并且将重复的放在末尾。具体地,unique
会返回去重后这个序列的末指针。
因此,我们将所有牌用 sort
排序后用一遍 unique
即可。
#include<bits/stdc++.h> // 引入标准库
using namespace std;
const int N=100; // 定义常量 N,表示最大牌数
int n; // 存储输入牌的数量
string s[N]; // 定义字符串数组,用于存储牌
signed main(){
ios::sync_with_stdio(0); cin.tie(0); // 优化输入输出
cin >> n; // 读入牌的数量
for(int i = 1; i <= n; i++) cin >> s[i]; // 读入每张牌
sort(s + 1, s + n + 1); // 对牌进行排序
n = unique(s + 1, s + n + 1) - s - 1; // 去重并更新牌的数量
cout << 52 - n; // 输出剩余牌的数量
return 0; // 程序结束
}
# 2. 使用 std::map
我们可以用内置平衡树的 map
来实现。可以用 map<string,bool> mp
来将一个字符串映射成一个 bool 型,而在代码的最后调用 mp.size()
即可返回有多少张本质不同的卡牌。
#include<bits/stdc++.h> // 引入标准库
using namespace std;
const int N=100; // 定义常量 N,表示最大牌数
int n; // 存储输入牌的数量
string s[N]; // 定义字符串数组,用于存储牌
map<string,bool> mp; // 定义 map,用于存储不同的牌
signed main(){
ios::sync_with_stdio(0); cin.tie(0); // 优化输入输出
cin >> n; // 读入牌的数量
for(int i = 1; i <= n; i++){ // 遍历每张牌
cin >> s[i]; // 读入牌
mp[s[i]] = 1; // 将牌映射到 map 中,值为 true
}
cout << 52 - mp.size(); // 输出剩余牌的数量
return 0; // 程序结束
}
# 3. 暴力法
使用一个布尔型二维数组 f[5][20]
来存对应花色是否有此点数,用函数 zhn
转换扑克牌的点数
#include <bits/stdc++.h> // 引入所有标准库
using namespace std;
int n, cnt; // n 为输入的牌数,cnt 用于计数缺失的牌
bool f[5][20]; // 第一维是花色(0-4),第二维是点数(0-19),用于标记牌的存在情况
// 按题意转换点数
int zhn(char x){
if('2' <= x && x <= '9') return x - '0'; // 点数为 2-9,直接返回其整数值
if(x == 'A') return 1; // A 转换为 1
if(x == 'T') return 10; // T 转换为 10
if(x == 'J') return 11; // J 转换为 11
if(x == 'Q') return 12; // Q 转换为 12
if(x == 'K') return 13; // K 转换为 13
}
int main(){
cin >> n; // 读入牌的数量
for(int i = 1; i <= n; i++){ // 遍历每张牌
char c1, c2; // c1 为花色,c2 为点数
cin >> c1 >> c2; // 读入花色和点数
// 根据花色设置标记
if(c1 == 'D') f[1][zhn(c2)] = 1; // 方块
else if(c1 == 'C') f[2][zhn(c2)] = 1; // 梅花
else if(c1 == 'H') f[3][zhn(c2)] = 1; // 红心
else if(c1 == 'S') f[4][zhn(c2)] = 1; // 黑桃
}
/* 遍历扑克牌,检查缺失的牌 */
for(int i = 1; i <= 4; i++){ // 遍历每种花色
for(int j = 1; j <= 13; j++){ // 遍历每种点数
if(!f[i][j]) cnt++; // 如果该牌不存在,计数器加一
}
}
cout << cnt; // 输出缺失的牌数
return 0; // 程序结束
}
# [CSP-J 2024] 地图探险
# 题目描述
小 A 打算前往一片丛林去探险。丛林的地理环境十分复杂,为了防止迷路,他先派遣了一个机器人前去探路。
丛林的地图可以用一个 行 列的字符表来表示。我们将第 行第 列的位置的坐标记作 。如果这个位置的字符为 ,即代表这个位置上有障碍,不可通过。反之,若这个位置的字符为 ,即代表这个位置是一片空地,可以通过。
这个机器人的状态由位置和朝向两部分组成。其中位置由坐标 刻画,它表示机器人处在地图上第 行第 列的位置。而朝向用一个 的 整数 表示,其中 代表向东, 代表向南, 代表向西, 代表向北。
初始时,机器人的位置为 ,朝向为 。保证初始时机器人所在的位置为空地。接下来机器人将要进行 次操作。每一步,机器人将按照如下的模式操作:
假设机器人当前处在的位置为 ,朝向为 。则它的方向上的下一步的位置 定义如下:若 ,则令 ;若 ,则令 ;若 ,则令 ;若 ,则令 。
接下来,机器人判断它下一步的位置是否在地图内,且是否为空地。具体地说,它判断 是否满足 ,,且 位置上是空地。如果条件成立,则机器人会向前走一步。它新的位置变为 ,且朝向不变。如果条件不成立,则它会执行“向右转”操作。也就是说,令 (即 除以 的余数),且它所处的位置保持不变,但朝向由 变为 。
小 A 想要知道,在机器人执行完 步操作之后,地图上所有被机器人经过的位置(包括起始位置)有几个。
# 输入格式
本题有多组测试数据。
输入的第一行包含一个正整数 ,表示数据组数。
接下来包含 组数据,每组数据的格式如下:
第一行包含三个正整数 。其中 表示地图的行数和列数, 表示机器人执行操作的次数。
第二行包含两个正整数 和一个非负整数 。
接下来 行,每行包含一个长度为 的字符串。保证字符串中只包含 和 两个字符。其中,第 行的字符串的第 个字符代表的位置为 。这个位置是 即代表它是障碍,否则代表它是空地。数据保证机器人初始时所在的位置为空地。
# 输出格式
对于每组数据:输出一行包含一个正整数,表示地图上所有被机器人经过的位置(包括起始位置)的个数。
# 样例 #1
# 样例输入 #1
2
1 5 4
1 1 2
....x
5 5 20
1 1 0
.....
.xxx.
.x.x.
..xx.
x....
# 样例输出 #1
3
13
# 提示
【样例 1 解释】
该样例包含两组数据。对第一组数据,机器人的状态以如下方式变化:
- 初始时,机器人位于位置 ,方向朝西(用数字 代表)。
- 第一步,机器人发现它下一步的位置 不在地图内,因此,它会执行“向右转”操作。此时,它的位置仍然为 ,但方向朝北(用数字 代表)。
- 第二步,机器人发现它下一步的位置 不在地图内,因此,它仍然会执行“向右转”操作。此时,它的位置仍然为 ,但方向朝东(用数字 代表)。
- 第三步,机器人发现它下一步的位置 在地图内,且为空地。因此,它会向东走一步。此时,它的位置变为 ,方向仍然朝东。
- 第四步,机器人发现它下一步的位置 在地图内,且为空地。因此,它会向东走一步。此时,它的位置变为 ,方向仍然朝东。
因此,四步之后,机器人经过的位置有三个,分别为 。
对第二组数据,机器人依次执行的操作指令为:向东走到 ,向东走到 ,向东走到 ,向东走到 ,向右转,向南走到 ,向南走到 ,向南走到 ,向南走到 ,向右转,向西走到 ,向西走到 ,向西走到 ,向右转,向北走到 ,向右转,向右转,向南走到 ,向右转,向右转。
【样例 2】
见选手目录下的 explore/explore2.in 与 explore/explore2.ans。
该样例满足第 个测试点的限制条件。
【样例 3】
见选手目录下的 explore/explore3.in 与 explore/explore3.ans。
该样例满足第 个测试点的限制条件。
【样例 4】
见选手目录下的 explore/explore4.in 与 explore/explore4.ans。
该样例满足第 个测试点的限制条件。
【样例 5】
见选手目录下的 explore/explore5.in 与 explore/explore5.ans。
该样例满足第 个测试点的限制条件。
【数据范围】
对于所有测试数据,保证:,,,,,且机器人的起始位置为空地。
测试点编号 | 特殊性质 | |||
---|---|---|---|---|
无 | ||||
无 | ||||
无 | ||||
无 | ||||
地图上所有位置均为空地 | ||||
无 | ||||
地图上所有位置均为空地 | ||||
无 | ||||
无 | ||||
无 |
# 解析
经典模拟题,对于模拟题,如果没有简单思路,直接暴力实现题目要求即可
题目背景
本题属于模拟题,为了解释清楚题意,题目长度偏长,放在这个位置可能对于年龄较低的参赛选手不太友好。
模拟要点
在代码中可以设置两个位移数组:
const int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0};
以简便地完成转向的移动。
注意横纵坐标的使用,尤其是变量名(如
y1
)不应定义为全局变量,以免出现编译错误。使用错误的横纵坐标可能导致通过样例但未通过评测的情况(例如将
y1
错写为y0
)。多测时请不要清空数据,爆零两行泪。
参考代码
#include <bits/stdc++.h> // 包含所有标准库
using namespace std;
bool vis[1005][1005]; // 访问标记数组,记录每个位置是否被访问过
char ch[1005][1005]; // 存储地图的字符数组
const int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0}; // 位移数组,用于控制方向(右、下、左、上)
void solve() {
int n, m, k, x0, y0, d0; // n: 行数, m: 列数, k: 移动步数, x0: 当前行, y0: 当前列, d0: 当前方向
memset(vis, 0, sizeof(vis)); // 初始化访问数组为false
cin >> n >> m >> k; // 输入行数、列数和移动步数
cin >> x0 >> y0 >> d0; // 输入起始位置(x0, y0)和初始方向(d0)
// 读取地图数据
for (int i = 1; i <= n; i++) {
char s[1005]; // 每一行的字符数组
cin >> s; // 输入一行地图
for (int j = 1; j <= m; j++)
ch[i][j] = s[j - 1]; // 将字符存入地图数组
}
vis[x0][y0] = true; // 标记起始位置为已访问
// 进行 k 步移动
for (int i = 1; i <= k; i++) {
int x1 = x0 + dx[d0], y1 = y0 + dy[d0]; // 计算下一个位置
// 检查下一个位置是否合法且未被障碍物阻挡
if (1 <= x1 && x1 <= n && 1 <= y1 && y1 <= m && ch[x1][y1] == '.') {
x0 = x1; // 更新当前位置
y0 = y1;
} else {
d0 = (d0 + 1) % 4; // 如果无法移动,顺时针转向
}
vis[x0][y0] = true; // 标记当前位置为已访问
}
int ans = 0; // 计数已访问的位置数量
// 统计已访问的位置
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
ans += vis[i][j]; // 累加已访问的数量
}
cout << ans << endl; // 输出结果
}
int main() {
int T; // 测试用例数量
cin >> T; // 输入测试用例数量
while (T--) {
solve(); // 处理每个测试用例
}
}
# [CSP-J 2024] 小木棍
# 题目描述
小 S 喜欢收集小木棍。在收集了 根长度相等的小木棍之后,他闲来无事,便用它们拼起了数字。用小木棍拼每种数字的方法如下图所示。

现在小 S 希望拼出一个正整数,满足如下条件:
- 拼出这个数恰好使用 根小木棍;
- 拼出的数没有前导 ;
- 在满足以上两个条件的前提下,这个数尽可能小。
小 S 想知道这个数是多少,可 很大,把木棍整理清楚就把小 S 折腾坏了,所以你需要帮他解决这个问题。如果不存在正整数满足以上条件,你需要输出 进行报告。
# 输入格式
本题有多组测试数据。
输入的第一行包含一个正整数 ,表示数据组数。
接下来包含 组数据,每组数据的格式如下:
一行包含一个整数 ,表示木棍数。
# 输出格式
对于每组数据:输出一行,如果存在满足题意的正整数,输出这个数;否则输出 。
# 样例 #1
# 样例输入 #1
5
1
2
3
6
18
# 样例输出 #1
-1
1
7
6
208
# 提示
【样例 1 解释】
- 对于第一组测试数据,不存在任何一个正整数可以使用恰好一根小木棍摆出,故输出 。
- 对于第四组测试数据,注意 并不是一个满足要求的方案。摆出 、 以及 都恰好需要 根小木棍,但它们不是摆出的数最小的方案。
- 对于第五组测试数据,摆出 需要 根小木棍。可以证明摆出任何一个小于 的正整数需要的小木棍数都不是 。注意尽管拼出 也需要 根小木棍,但因为这个数有前导零,因此并不是一个满足要求的方案。
【数据范围】
对于所有测试数据,保证:,。
测试点编号 | 特殊性质 | |
---|---|---|
无 | ||
无 | ||
A | ||
A | ||
B | ||
B | ||
无 | ||
无 |
特殊性质 A:保证 是 的倍数且 。
特殊性质 B:保证存在整数 使得 ,且 。
# 解析
# 题意概括
给定一个自然数 ,要求用 根木棍摆出最小的数字(不含前导 0)。
# 思路
可以通过分类讨论并考虑 N \mod 7 的余数来解决问题:
余数为 0:答案为 个 8。
余数为 1:
- 特判: 时无答案,输出 -1。
- 其他情况:答案为 10 拼接 个 8。
余数为 2:答案为 1 和 个 8。
余数为 3:
- 特判: 时答案为 7。
- 特判: 时答案为 22。
- 其他情况:答案为 200 拼接 个 8。
余数为 4:
- 特判: 时答案为 4。
- 其他情况:答案为 20 拼接 个 8。
余数为 5:答案为 2 拼接 个 8。
余数为 6:答案为 6 拼接 个 8。
# 代码
#include<bits/stdc++.h> // 引入头文件
using namespace std; // 使用标准命名空间
long long n, t, ans, stick[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6}; // 声明变量
int main() { // 主函数
cin >> t; // 输入测试用例数量
while (t--) { // 遍历每个测试用例
cin >> n; // 输入自然数 N
ans = 1e9 + 1; // 初始化答案为一个很大的数
if (n == 1) cout << -1; // 特判:N=1时无答案
else if (n == 2) cout << 1; // 特判:N=2时答案为1
else if (n == 3) cout << 7; // 特判:N=3时答案为7
else if (n == 4) cout << 4; // 特判:N=4时答案为4
else if (n == 5) cout << 2; // 特判:N=5时答案为2
else if (n == 6) cout << 6; // 特判:N=6时答案为6
else if (n == 7) cout << 8; // 特判:N=7时答案为8
else if (n % 7 == 0) { // 余数为0的情况
for (int i = 1; i <= n / 7; i++) cout << 8; // 输出 N/7 个 8
}
else if (n % 7 == 1) { // 余数为1的情况
cout << 10; // 输出10
for (int i = 1; i <= (n - 8) / 7; i++) cout << 8; // 输出 (N-8)/7 个 8
}
else if (n % 7 == 2) { // 余数为2的情况
cout << 1; // 输出1
for (int i = 1; i <= (n - 2) / 7; i++) cout << 8; // 输出 (N-2)/7 个 8
}
else if (n % 7 == 3) { // 余数为3的情况
if (n == 10) cout << 22; // 特判:N=10时答案为22
else {
cout << 200; // 输出200
for (int i = 1; i <= (n - 17) / 7; i++) cout << 8; // 输出 (N-17)/7 个 8
}
}
else if (n % 7 == 4) { // 余数为4的情况
cout << 20; // 输出20
for (int i = 1; i <= (n - 11) / 7; i++) cout << 8; // 输出 (N-11)/7 个 8
}
else if (n % 7 == 5) { // 余数为5的情况
cout << 2; // 输出2
for (int i = 1; i <= (n - 5) / 7; i++) cout << 8; // 输出 (N-5)/7 个 8
}
else if (n % 7 == 6) { // 余数为6的情况
cout << 6; // 输出6
for (int i = 1; i <= (n - 6) / 7; i++) cout << 8; // 输出 (N-6)/7 个 8
}
cout << "\n"; // 输出换行
}
return 0; // 返回0表示程序正常结束
}
# [CSP-J 2024] 接龙
# 题目描述
在玩惯了成语接龙之后,小 J 和他的朋友们发明了一个新的接龙规则。
总共有 个人参与这个接龙游戏,第 个人会获得一个整数序列 作为他的词库。
一次游戏分为若干轮,每一轮规则如下:
- 个人中的某个人 带着他的词库 进行接龙。若这不是游戏的第一轮,那么这一轮进行接龙的人不能与上一轮相同,但可以与上上轮或更往前的轮相同。
- 接龙的人选择一个长度在 的 的连续子序列 作为这一轮的接龙序列,其中 是给定的常数。若这是游戏的第一轮,那么 需要以元素 开头,否则 需要以上一轮的接龙序列的最后一个元素开头。
- 序列 是序列 的连续子序列当且仅当可以通过删除 的开头和结尾的若干元素(可以不删除)得到 。
为了强调合作,小 J 给了 个参与游戏的人 个任务,第 个任务需要这 个人进行一次游戏,在这次游戏里进行恰好 轮接龙,且最后一轮的接龙序列的最后一个元素恰好为 。为了保证任务的可行性,小 J 请来你判断这 个任务是否可以完成的,即是否存在一个可能的游戏过程满足任务条件。
# 输入格式
本题有多组测试数据。
输入的第一行包含一个正整数 ,表示数据组数。
接下来包含 组数据,每组数据的格式如下:
第一行包含三个整数 ,分别表示参与游戏的人数、接龙序列长度上限以及任务个数。
接下来 行:
第 行包含 个整数 ,其中第一个整数 表示序列 的长度,接下来 个整数描述序列 。
接下来 行:
第 行包含两个整数 ,描述一个任务。
# 输出格式
对于每个任务:输出一行包含一个整数,若任务可以完成输出 1,否则输出 0。
# 样例 #1
# 样例输入 #1
1
3 3 7
5 1 2 3 4 1
3 1 2 5
3 5 1 6
1 2
1 4
2 4
3 4
6 6
1 1
7 7
# 样例输出 #1
1
0
1
0
1
0
0
# 提示
【样例 1 解释】
在下文中,我们使用 表示一轮游戏中所有的接龙序列, 表示对应的接龙的人的编号。由于所有字符均为一位数字,为了方便我们直接使用数字字符串表示序列。
- 对于第一组询问,、 是一个满足条件的游戏过程。
- 对于第二组询问,可以证明任务不可完成。注意 、 不是合法的游戏过程,因为此时 。
- 对于第三组询问,、 是一个满足条件的游戏过程。
- 对于第四组询问,可以证明任务不可完成。注意 不是一个合法的游戏过程,因为尽管所有的接龙序列长度均不超过 ,但第二轮和第三轮由同一个人接龙,不符合要求。
- 对于第五组询问,、 是一个满足条件的游戏过程。
- 对于第六组询问,可以证明任务不可完成。注意每个接龙序列的长度必须大于等于 ,因此 不是一个合法的游戏过程。
- 对于第七组询问,所有人的词库均不存在字符 ,因此任务显然不可完成。
【样例 2】
见选手目录下的 chain/chain2.in 与 chain/chain2.ans。
该样例满足测试点 1 的特殊性质。
【样例 3】
见选手目录下的 chain/chain3.in 与 chain/chain3.ans。
该样例满足测试点 2 的特殊性质。
【样例 4】
见选手目录下的 chain/chain4.in 与 chain/chain4.ans。
该样例满足特殊性质 A,其中前两组测试数据满足 、、单组测试数据内所有词库的长度和 、。
【样例 5】
见选手目录下的 chain/chain5.in 与 chain/chain5.ans。
该样例满足特殊性质 B,其中前两组测试数据满足 、、单组测试数据内所有词库的长度和 、。
【样例 6】
见选手目录下的 chain/chain6.in 与 chain/chain6.ans。
该样例满足特殊性质 C,其中前两组测试数据满足 、、单组测试数据内所有词库的长度和 、。
【数据范围】
对于所有测试数据,保证:
- ;
- ,,;
- ,;
- ,;
- 设 为单组测试数据内所有 的和,则 。
测试点 | 特殊性质 | ||||
---|---|---|---|---|---|
无 | |||||
无 | |||||
A | |||||
A | |||||
B | |||||
B | |||||
C | |||||
C | |||||
无 | |||||
无 |
特殊性质 A:保证 。
特殊性质 B:保证 k ≤ 5。
特殊性质 C:保证在单组测试数据中,任意一个字符在词库中出现次数之和均不超过 。
# 解析
这是一道 CSP-S 第 1.5 题难度的动态规划,但是放在 CSP-J 来考严格来说也没有超纲。
问题描述:
- S 由所有人的拥有的序列首尾相接而成。
- 元素 S[j] 属于第 person[j] 个人。
- 第 i 个人与第 i+1 个人拥有的序列在 S 中的分界线为 end[i]。
数据结构:
head[round][j]
表示第 round 轮以元素 S[j] 开头是否可行。tail[round][j]
表示第 round 轮以元素 S[j] 结尾是否可行。headSum
是第 round 轮中 head[round] 的左闭右开前缀和。owner[round][x]
:- 为 0 表示第 round 轮没有人能以值为 x 的元素结尾。
- 为正整数表示第 round 轮第 owner[round][x] 个人能以值为 x 的元素结尾。
- 为正无穷表示第 round 轮有超过一个人能以值为 x 的元素结尾。
#include <iostream> // 引入输入输出流库
#include <climits> // 引入限制常量库
#include <algorithm> // 引入算法库
namespace noip { // 定义命名空间 noip
typedef long long Int; // 定义长整型别名 Int
constexpr Int maxN = 100'000; // 定义最大人数
constexpr Int maxR = 100; // 定义最大轮数
constexpr Int maxLSum = 200'000; // 定义最大序列长度
constexpr Int maxS = 200'000; // 定义最大元素值
constexpr Int veryBeginningS = 1; // 定义序列开始元素的值
Int S[maxLSum], person[maxLSum]; // 定义序列和每个元素所属的人
bool tail[1+maxR][maxLSum], head[1+maxR][maxLSum]; // 定义头尾可行性数组
Int owner[1+maxR][1+maxS]; // 定义每轮中每个元素的拥有者
Int n, k, q; // 定义人数、序列长度、查询次数
Int l[1+maxN], end[1+maxN]; // 定义每个人的序列长度和序列的结束索引
void main() { // 主函数
std::cin >> n >> k >> q; // 输入人数、序列长度、查询次数
end[0] = 0; // 初始化序列结束索引
for (Int i = 1; i <= n; i++) { // 遍历每个人
end[i] = end[i-1]; // 更新当前人的结束索引
std::cin >> l[i]; // 输入当前人的序列长度
for (Int j = 1; j <= l[i]; j++) std::cin >> S[end[i]++]; // 输入当前人的序列元素
for (Int j = end[i-1]; j < end[i]; j++) person[j] = i; // 记录每个元素所属的人
}
for (Int round = 1; round <= maxR; round++) { // 遍历每一轮
if (round > 1) // 如果不是第一轮
for (Int j = 0; j < end[n]; j++) head[round][j] = // 更新头可行性
owner[round-1][S[j]] == LLONG_MAX ? true : // 如果上轮是无穷大,表示可行
owner[round-1][S[j]] == 0 ? false : // 如果上轮是0,表示不可行
owner[round-1][S[j]] != person[j]; // 否则根据所属人判断可行性
else // 如果是第一轮
for (Int j = 0; j < end[n]; j++)
head[round][j] = S[j] == veryBeginningS; // 仅当元素是起始元素时可行
static Int headSum[1+maxLSum+1]; // 定义头前缀和数组
headSum[0] = 0; // 初始化前缀和
for (Int j = 0; j < end[n]; j++) headSum[j+1] = headSum[j] + head[round][j]; // 计算前缀和
for (Int right = 0; right < end[n]; right++) { // 遍历序列元素
Int left = std::max(end[person[right]-1], right-k+1); // 计算左边界
tail[round][right] = headSum[right] - headSum[left]; // 更新尾可行性
}
std::fill(owner[round]+1, owner[round]+1+maxS, 0); // 初始化拥有者数组
for (Int j = 0; j < end[n]; j++) if (tail[round][j]) owner[round][S[j]] = // 更新拥有者
owner[round][S[j]] == 0 ? person[j] : // 如果之前没有人,记录当前人
owner[round][S[j]] == person[j] ? person[j] : // 如果是同一个人,保持不变
LLONG_MAX; // 否则标记为无穷大
}
while (q--) { // 处理查询
Int r, c; std::cin >> r >> c; // 输入查询的轮次和元素值
std::cout << (Int)bool(owner[r][c]) << std::endl; // 输出结果
}
}
}
int main() { // 主入口函数
noip::Int T; std::cin >> T; // 输入测试案例数
while (T--) noip::main(); // 处理每个测试案例
return 0; // 返回0,表示程序正常结束
}