P11230 [CSP-J 2024] 接龙

发现所有题解都是用dp来做的,而且还比较有难度。

这些状态和如何$ O(1) $转移其实还是有难度的,所以本蒟蒻来写一个伟大的cpp教的非dp算法。

首先发现问题的切入点在于 r 最大只有一百,思考如何按轮次顺序预处理出所有的情况。

\(r=1\)时,寻找开头为一的长度在 \([2,k]\) 内的序列,将这些序列的末尾全部标记为1即可。

那么下一轮转移不就很简单了吗,只需要遍历所有的数字,如果这个值可以作为上一轮的末尾,那么将它后面的点再次标记。

但是问题在于我并不知道它的上一步是否和它属于同一个人,如果属于,那么这次操作是不可行的。

其实想解决这个问题并不难,我们可以分析一下每个点作为开头的情况:

1.上一步无法到达,不进行任何操作。

2.上一步仅有一个数列可以到达,判断是否在一列,若在,不操作,若在,操作。

3.上一步有两个不同数列可以到达,直接进行操作。

综上所述,仅需记录每一次是否能够达到,若仅有一个数列可以达到,记录下这一个数列。

那么我们不妨将 $ ans_{i,j} $设置为第i轮后末尾为j的情况,初始值设为-1,其中-1时表示无法到达,0表示有两个不同的数列可以到达,其他数字表示唯一一个可以到达的数列的编号。

但是每次找到一个点就要再次遍历k次后面的数,所以时间肯定是不够的。

不难发现出现了重复遍历,所以我们可以记一个变量 tmp 表示后面有多少个点在本轮中能够到达,每次发现一个点可以作为本轮的起点时,就将其设为 \(k-1\) ,如果k不为零,那就说明这个值在本轮可以作为答案,每次发现一个新答案后 tmp 减一即可。

之后就不必过多赘述了。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,n,k,q,r,c;
vector<int> e[100005];
int ans[105][200005];
void init(){
	for(int i=1;i<=n;i++)e[i].clear();
	memset(ans,-1,sizeof(ans));
}
void solve(){
	for(int i=1,tmp;i<=n;i++){//第一轮 
		tmp=0;
		for(int j:e[i]){
			if(tmp){ 
				tmp--;
				if(ans[1][j]==-1)ans[1][j]=i;
				else if(ans[1][j]!=i)ans[1][j]=0;
			}
			if(j==1)tmp=k-1;//记录后面是否存为答案,下同 
		}
	}
	for(int l=2;l<=100;l++){
		for(int i=1,tmp;i<=n;i++){
			tmp=0;
			for(int j:e[i]){
				if(tmp){
					tmp--;
					if(ans[l][j]==-1)ans[l][j]=i;
					else if(ans[l][j]!=i)ans[l][j]=0;
				}
				if(ans[l-1][j]!=-1&&ans[l-1][j]!=i)tmp=k-1;
			}
		}
	}
}
signed main(){
	scanf("%lld",&t);
	while(t--){
		scanf("%lld%lld%lld",&n,&k,&q);
		init();//cyt在次提醒您:数据千万条,清空第一条。多测不清空,暴零泪两行 。 
		for(int i=1,l,x;i<=n;i++){
			scanf("%lld",&l);
			while(l--){
				scanf("%lld",&x);
				e[i].push_back(x);//将每个数加入相应队列
			}
		}
		solve();
		while(q--){
			scanf("%lld%lld",&r,&c);
			if(ans[r][c]!=-1)printf("1\n");
			else printf("0\n");
		}
	}
	return 0;
}
posted @ 2025-10-31 08:53  huhangqi  阅读(11)  评论(0)    收藏  举报
/*
*/