信息学奥赛【CSP-S 2022】真题解析

发布时间 2023-10-11 18:38:56作者: surprise_ying

T1假期计划

[CSP-S 2022] 假期计划

题目描述

小熊的地图上有 \(n\) 个点,其中编号为 \(1\) 的是它的家、编号为 \(2, 3, \ldots, n\) 的都是景点。部分点对之间有双向直达的公交线路。如果点 \(x\)\(z_1\)\(z_1\)\(z_2\)、……、\(z_{k - 1}\)\(z_k\)\(z_k\)\(y\) 之间均有直达的线路,那么我们称 \(x\)\(y\) 之间的行程可转车 \(k\) 次通达;特别地,如果点 \(x\)\(y\) 之间有直达的线路,则称可转车 \(0\) 次通达。

很快就要放假了,小熊计划从家出发去 \(4\)不同的景点游玩,完成 \(5\) 段行程后回家:家 \(\to\) 景点 A \(\to\) 景点 B \(\to\) 景点 C \(\to\) 景点 D \(\to\) 家且每段行程最多转车 \(k\) 次。转车时经过的点没有任何限制,既可以是家、也可以是景点,还可以重复经过相同的点。例如,在景点 A \(\to\) 景点 B 的这段行程中,转车时经过的点可以是家、也可以是景点 C,还可以是景点 D \(\to\) 家这段行程转车时经过的点。

假设每个景点都有一个分数,请帮小熊规划一个行程,使得小熊访问的四个不同景点的分数之和最大。

输入格式

第一行包含三个正整数 \(n, m, k\),分别表示地图上点的个数、双向直达的点对数量、每段行程最多的转车次数。

第二行包含 \(n - 1\) 个正整数,分别表示编号为 \(2, 3, \ldots, n\) 的景点的分数。

接下来 \(m\) 行,每行包含两个正整数 \(x, y\),表示点 \(x\)\(y\) 之间有道路直接相连,保证 \(1 \le x, y \le n\),且没有重边,自环。

输出格式

输出一个正整数,表示小熊经过的 \(4\) 个不同景点的分数之和的最大值。

样例 #1

样例输入 #1

8 8 1
9 7 1 8 2 3 6
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 1

样例输出 #1

27

样例 #2

样例输入 #2

7 9 0
1 1 1 2 3 4
1 2
2 3
3 4
1 5
1 6
1 7
5 4
6 4
7 4

样例输出 #2

7

提示

【样例解释 #1】

当计划的行程为 \(1 \to 2 \to 3 \to 5 \to 7 \to 1\) 时,\(4\) 个景点的分数之和为 \(9 + 7 + 8 + 3 = 27\),可以证明其为最大值。

行程 \(1 \to 3 \to 5 \to 7 \to 8 \to 1\) 的景点分数之和为 \(24\)、行程 \(1 \to 3 \to 2 \to 8 \to 7 \to 1\) 的景点分数之和为 \(25\)。它们都符合要求,但分数之和不是最大的。

行程 \(1 \to 2 \to 3 \to 5 \to 8 \to 1\) 的景点分数之和为 \(30\),但其中 \(5 \to 8\) 至少需要转车 \(2\) 次,因此不符合最多转车 \(k = 1\) 次的要求。

行程 \(1 \to 2 \to 3 \to 2 \to 3 \to 1\) 的景点分数之和为 \(32\),但游玩的并非 \(4\) 个不同的景点,因此也不符合要求。

【样例 #3】

见附件中的 holiday/holiday3.inholiday/holiday3.ans

【数据范围】

对于所有数据,保证 \(5 \le n \le 2500\)\(1 \le m \le 10000\)\(0 \le k \le 100\),所有景点的分数 \(1 \le s_i \le {10}^{18}\)。保证至少存在一组符合要求的行程。

测试点编号 \(n \le\) \(m \le\) \(k \le\)
\(1 \sim 3\) \(10\) \(20\) \(0\)
\(4 \sim 5\) \(10\) \(20\) \(5\)
\(6 \sim 8\) \(20\) \(50\) \(100\)
\(9 \sim 11\) \(300\) \(1000\) \(0\)
\(12 \sim 14\) \(300\) \(1000\) \(100\)
\(15 \sim 17\) \(2500\) \(10000\) \(0\)
\(18 \sim 20\) \(2500\) \(10000\) \(100\)

解析

【算法1】首先需要预处理出任意点对之间的距离,显然很容易想到Floyd算法,并能以O(n^3)的算法复杂度预处理出任意点对之间的距离,然后即可用dfs暴力更新出所有可能的路径。算法复杂度O(n!),预计得分25分,实际得分60分,参考代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=2555;
typedef long long ll;
int n,m,k;
int a[N][N],b[N],d[N][N],vis[N],c[N]; 
ll ans;
void dfs(int u,int pre,ll sum)
{

//	printf("u=%d,pre=%d\n",u,pre);
	if(u==5)
	{
		
		if(a[pre][1]>k+1) return ;
		if(ans<sum)
		{
			ans=sum;
//			for(int i=1;i<=4;i++) cout<<c[i]<<" ";cout<<endl;
		}
		return ;
	}
	for(int i=2;i<=n;i++)
	{
		if(vis[i]) continue;
		if(a[pre][i]<=k+1)
		{
			vis[i]=1;
			c[u]=i;
			dfs(u+1,i,sum+b[i]);
			vis[i]=0;
		}
	}
}
int main()
{
	memset(a,0x3f,sizeof(a));
	cin>>n>>m>>k;
	for(int i=2;i<=n;i++) cin>>b[i];
	for(int i=1;i<=n;i++) a[i][i]=0;
	for(int i=1;i<=m;i++)
	{
		int x,y;
		cin>>x>>y;
		a[x][y]=a[y][x]=1;
	}
	for(int kk=1;kk<=n;kk++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				a[i][j]=min(a[i][j],a[i][kk]+a[kk][j]);
	dfs(1,1,0);
	cout<<ans<<endl;
	return 0;	
} 

【算法2】考虑优化预处理任意点对之间的距离,由于是无向图,完全可以暴力枚举起点,然后对这个起点跑一遍bfs预处理出任何一个点到当前点的距离,显然这样做的复杂度是O(n*(n+m))。由于题目固定是经过4个景点,所以直接暴力枚举这4个中间点即可,算法复杂度O(n^4),预计得分70分,实际得分60分。

#include<bits/stdc++.h>
using namespace std;
const int N=2505;
int n,m,k;
int d[N][N];
long long a[N];
vector<int>g[N]; 
void bfs(int s)
{
	queue<pair<int,int> >q;
	q.push(make_pair(s,-1));
	int vis[N]={0};
	vis[s]=1;
	d[s][s]=-1;
	while(q.size())
	{
		int u=q.front().first;q.pop();
		for(int i=0;i<g[u].size();i++)
		{
			int v=g[u][i];
			if(vis[v]) continue;
			d[s][v]=d[s][u]+1;
			vis[v]=1;
			q.push(make_pair(v,d[s][v]));
		}
	}
}
int main()
{
	cin>>n>>m>>k;
	for(int i=2;i<=n;i++) cin>>a[i];
	memset(d,0x3f,sizeof(d));
	for(int i=1;i<=m;i++)
	{
		int x,y;
		cin>>x>>y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	for(int i=1;i<=n;i++) bfs(i);
//	for(int i=1;i<=n;i++)
//	{
//		for(int j=1;j<=n;j++)
//			printf("d[%d][%d]=%d ",i,j,d[i][j]);
//		printf("\n");
//	}
	int vis[N]={0};
	long long ans=0;
	for(int v1=2;v1<=n;v1++)
	{
		if(d[1][v1]>k) continue;
		vis[v1]=1;
		for(int v2=2;v2<=n;v2++)
		{
			if(d[v1][v2]>k||vis[v2]) continue;
			vis[v2]=1;
			for(int v3=2;v3<=n;v3++)
			{
				if(d[v2][v3]>k||vis[v3]) continue;
				vis[v3]=1;
				for(int v4=2;v4<=n;v4++)
				{
					if(d[v3][v4]>k||d[v4][1]||vis[v4]) continue;
					ans=max(ans,a[v1]+a[v2]+a[v3]+a[v4]);
				}
				vis[v3]=0; 
			}
			vis[v2]=0;
		}
		vis[v1]=0;
	}
	cout<<ans<<endl;
	return 0;	
}

在这里学习一中避免TLE的方法。由于这里是最优化问题,在更新的过程中有一定的概率已经更新出了最优解,所以在快超时的时候,与其让它超时,还不如直接输出当前答案。
代码中值得一提的是使用了CLOCKS_PER_SEC这个常量,含义是1秒。代码clock()-start<2*CLOCKS_PER_SEC*0.95的含义是当前时间减去程序开始的时间小于时限的95%的意思。参考代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=2505;
int n,m,k;
int d[N][N];
long long a[N];
vector<int>g[N]; 
double start=clock();
int check()
{
	return clock()-start<2*CLOCKS_PER_SEC*0.95;
}
void bfs(int s)
{
	queue<pair<int,int> >q;
	q.push(make_pair(s,-1));
	int vis[N]={0};
	vis[s]=1;
	d[s][s]=-1;
	while(q.size())
	{
		int u=q.front().first;q.pop();
		for(int i=0;i<g[u].size();i++)
		{
			int v=g[u][i];
			if(vis[v]) continue;
			d[s][v]=d[s][u]+1;
			vis[v]=1;
			q.push(make_pair(v,d[s][v]));
		}
	}
}
int main()
{
	cin>>n>>m>>k;
	for(int i=2;i<=n;i++) cin>>a[i];
	memset(d,0x3f,sizeof(d));
	for(int i=1;i<=m;i++)
	{
		int x,y;
		cin>>x>>y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	for(int i=1;i<=n;i++) bfs(i);
//	for(int i=1;i<=n;i++)
//	{
//		for(int j=1;j<=n;j++)
//			printf("d[%d][%d]=%d ",i,j,d[i][j]);
//		printf("\n");
//	}
	int vis[N]={0};
	long long ans=0;
	for(int v1=2;v1<=n;v1++)
	{
		if(!check()) break;
		if(d[1][v1]>k) continue;
		vis[v1]=1;
		for(int v2=2;v2<=n;v2++)
		{
			if(!check()) break;
			if(d[v1][v2]>k||vis[v2]) continue;
			vis[v2]=1;
			for(int v3=2;v3<=n;v3++)
			{
				if(!check()) break;
				if(d[v2][v3]>k||vis[v3]) continue;
				vis[v3]=1;
				for(int v4=2;v4<=n;v4++)
				{
					if(!check()) break;
					if(d[v3][v4]>k||d[v4][1]||vis[v4]) continue;
					ans=max(ans,a[v1]+a[v2]+a[v3]+a[v4]);
				}
				vis[v3]=0; 
			}
			vis[v2]=0;
		}
		vis[v1]=0;
	}
	cout<<ans<<endl;
	return 0;	
}

【正解】预处理的时间复杂度显然已经满足题目数据量要求了,接下来考虑优化直接暴力枚举4个景点的部分。不难想到,4个景点可以折半搜索。具体怎么做呢?对于1->a->b->c->d->1。可以先暴力枚举出1能到达的任意两个景点(a,b)以及1->a->b的路径最短长度。接下来可以发现1->d->c也可以跟前面一样处理,但是有可能会出现d=a,c=b的这种非法情况。稍微思考一下就可以发现,我们只要保存1能到达的任意两个景点(a,b)以及1->a->b的路径前3短的长度及其对应的顶点就绝对能找出4个景点不同的1->a->b->c->d->1路线。这样我们就先以O(n2)的算法复杂度预处理出前半段,再以O(n2)的复杂度匹配后半段即可。总算法复杂度O(n*(n+m)+n^2)代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=2555;
typedef long long ll;
int n,m,k;
ll a[N];
int d[N][N];
pair<ll,int>Max[N][3];
vector<int>g[N];
void bfs(int s)
{
	queue<pair<int,int> >q;
	q.push(make_pair(s,-1));
	int vis[N]={0};
	d[s][s]=-1;
	vis[s]=1;
	while(q.size())
	{
		int u=q.front().first;q.pop();
		for(int i=0;i<g[u].size();i++)
		{
			int v=g[u][i];
			if(vis[v]==0)
			{
				vis[v]=1;
				d[s][v]=d[s][u]+1;
				q.push(make_pair(v,d[s][v]));
			}
		}
	}
}
int main()
{
	cin>>n>>m>>k;
	for(int i=2;i<=n;i++) cin>>a[i];
	for(int i=1;i<=m;i++)
	{
		int x,y;
		cin>>x>>y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	memset(d,0x3f,sizeof(d));
	for(int i=1;i<=n;i++) bfs(i);
//	for(int i=1;i<=n;i++)
//		for(int j=1;j<=n;j++) printf("%d%c",d[i][j],j==n?'\n':' ');
	for(int i=2;i<=n;i++)
	{
		if(d[1][i]>k) continue;
		for(int j=2;j<=n;j++)
		{
			if(d[i][j]>k||i==j) continue;
			if(a[i]+a[j]>Max[j][0].first)
			{
				Max[j][2]=Max[j][1];
				Max[j][1]=Max[j][0];
				Max[j][0]=make_pair(a[i]+a[j],i);
			}
			else if(a[i]+a[j]>Max[j][1].first)
			{
				Max[j][2]=Max[j][1];
				Max[j][1]=make_pair(a[i]+a[j],i);
			}
			else if(a[i]+a[j]>Max[j][2].first)
			{
				Max[j][2]=make_pair(a[i]+a[j],i);
			}
		}
	}
	ll ans=0;
	for(int i=2;i<=n;i++)
	{
		for(int j=2;j<=n;j++)
		{
			if(d[i][j]>k||i==j) continue;
			for(int p=0;p<3;p++)
			{
				for(int q=0;q<3;q++)
				{
					int v1=Max[i][p].second;ll w1=Max[i][p].first;
					int v4=Max[j][q].second;ll w2=Max[j][q].first;
					if(v1!=v4&&v1!=i&&v1!=j&&v4!=i&&v4!=j&&d[v4][1]<=k&&d[v1][1]<=k)
					{
						ans=max(ans,w1+w2);
					}
				}
			}
		}
	}
	cout<<ans<<endl;
	return 0;	
} 

T2 策略游戏

[CSP-S 2022] 策略游戏

题目描述

小 L 和小 Q 在玩一个策略游戏。

有一个长度为 \(n\) 的数组 \(A\) 和一个长度为 \(m\) 的数组 \(B\),在此基础上定义一个大小为 \(n \times m\) 的矩阵 \(C\),满足 \(C_{i j} = A_i \times B_j\)。所有下标均从 \(1\) 开始。

游戏一共会进行 \(q\) 轮,在每一轮游戏中,会事先给出 \(4\) 个参数 \(l_1, r_1, l_2, r_2\),满足 \(1 \le l_1 \le r_1 \le n\)\(1 \le l_2 \le r_2 \le m\)

游戏中,小 L 先选择一个 \(l_1 \sim r_1\) 之间的下标 \(x\),然后小 Q 选择一个 \(l_2 \sim r_2\) 之间的下标 \(y\)。定义这一轮游戏中二人的得分是 \(C_{x y}\)

小 L 的目标是使得这个得分尽可能大,小 Q 的目标是使得这个得分尽可能小。同时两人都是足够聪明的玩家,每次都会采用最优的策略。

请问:按照二人的最优策略,每轮游戏的得分分别是多少?

输入格式

第一行输入三个正整数 \(n, m, q\),分别表示数组 \(A\),数组 \(B\) 的长度和游戏轮数。

第二行:\(n\) 个整数,表示 \(A_i\),分别表示数组 \(A\) 的元素。

第三行:\(m\) 个整数,表示 \(B_i\),分别表示数组 \(B\) 的元素。

接下来 \(q\) 行,每行四个正整数,表示这一次游戏的 \(l_1, r_1, l_2, r_2\)

输出格式

输出共 \(q\) 行,每行一个整数,分别表示每一轮游戏中,小 L 和小 Q 在最优策略下的得分。

样例 #1

样例输入 #1

3 2 2
0 1 -2
-3 4
1 3 1 2
2 3 2 2

样例输出 #1

0
4

样例 #2

样例输入 #2

6 4 5
3 -1 -2 1 2 0
1 2 -1 -3
1 6 1 4
1 5 1 4
1 4 1 2
2 6 3 4
2 5 2 3

样例输出 #2

0
-2
3
2
-1

提示

【样例解释 #1】

这组数据中,矩阵 \(C\) 如下:

\[\begin{bmatrix} 0 & 0 \\ -3 & 4 \\ 6 & -8 \end{bmatrix} \]

在第一轮游戏中,无论小 L 选取的是 \(x = 2\) 还是 \(x = 3\),小 Q 都有办法选择某个 \(y\) 使得最终的得分为负数。因此小 L 选择 \(x = 1\) 是最优的,因为这样得分一定为 \(0\)

而在第二轮游戏中,由于小 L 可以选 \(x = 2\),小 Q 只能选 \(y = 2\),如此得分为 \(4\)

【样例 #3】

见附件中的 game/game3.ingame/game3.ans

【样例 #4】

见附件中的 game/game4.ingame/game4.ans

【数据范围】

对于所有数据,\(1 \le n, m, q \le {10}^5\)\(-{10}^9 \le A_i, B_i \le {10}^9\)。对于每轮游戏而言,\(1 \le l_1 \le r_1 \le n\)\(1 \le l_2 \le r_2 \le m\)

测试点编号 \(n, m, q \le\) 特殊条件
\(1\) \(200\) 1, 2
\(2\) \(200\) 1
\(3\) \(200\) 2
\(4 \sim 5\) \(200\)
\(6\) \(1000\) 1, 2
\(7 \sim 8\) \(1000\) 1
\(9 \sim 10\) \(1000\) 2
\(11 \sim 12\) \(1000\)
\(13\) \({10}^5\) 1, 2
\(14 \sim 15\) \({10}^5\) 1
\(16 \sim 17\) \({10}^5\) 2
\(18 \sim 20\) \({10}^5\)

其中,特殊性质 1 为:保证 \(A_i, B_i > 0\)
特殊性质 2 为:保证对于每轮游戏而言,要么 \(l_1 = r_1\),要么 \(l_2 = r_2\)

解析

首先,考虑暴力枚举,算法复杂度O(qnm),预计得分25分,实际得分60分。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,q,a[N],b[N];
int main()
{
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=m;i++) cin>>b[i];
	while(q--)
	{
		int l1,r1,l2,r2;
		cin>>l1>>r1>>l2>>r2;
		long long ans=-1e18;
		for(int i=l1;i<=r1;i++)
		{
			long long tmp=1e18;
			for(int j=l2;j<=r2;j++)
			{
				tmp=min(tmp,1ll*a[i]*b[j]);
			}
			ans=max(ans,tmp);
		}
		cout<<ans<<endl;
	}
	return 0;	
} 

考虑正解。肯定先思考 B 再思考 A,因为 A 会思考 B 的思考。
B 的行为就是对于 \(x\),找到一个 \(b[l_2 \cdots r_2]\) 中的 \(y\),使得 \(x \times y\) 最小。
具体地:

\(x \ge 0\) 时,B 会选择最小的 \(y\)
\(x < 0\) 时,B 会选择最大的 \(y\)

那么 A 的行为是什么呢?还是按照正负分类讨论:
如果 A 这次想让 \(x \ge 0\),那么 B 会选择最小的 \(y\)。如果这个 \(y \ge 0\),那么 A 一定会选最大的 \(x\);如果这个 \(y < 0\),那么 A 一定会选最小的非负数 \(x\)(别忘了当前制约条件 \(x \ge 0\))。
如果 A 这次想让 \(x < 0\),那么 B 会选择最大的 \(y\)。如果这个 \(y \ge 0\),那么 A 一定会选最大的负数 \(x\);如果这个 \(y <0\),那么 A 一定会选最小的 \(x\)
因此 A 的行为只有四种:选择最大的 \(x\);最小的 \(x\),最大的负数 \(x\),最小的非负数 \(x\)
分别讨论 A 选择四种行为时 B 的选择,答案取最大值即可。
然后就变成了静态区间最值的板子。使用 6 个 ST 表分别存储以下信息:

\(a\) 的区间最大值;
\(a\) 的区间最小值;
\(a\) 的负数区间最大值;

具体是把所有满足 \(a_i \ge 0\)\(a_i\) 全部替换为 \(-\infty\) 代表这个位置不存在数,至于为何是 \(-\infty\) 请读者自己思考。

\(a\) 的非负数区间最小值;

具体是把所有满足 \(a_i < 0\)\(a_i\) 全部替换为 \(+\infty\) 代表这个位置不存在数。

\(b\) 的区间最大值;
\(b\) 的区间最小值。

时间复杂度 \(\mathcal{O}(n\log n + q)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int n,m,q;
ll amax[N][20],amin[N][20],bmax[N][20],bmin[N][20],azmin[N][20],afmax[N][20],lg[N];
int main()
{
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++) 
	{
		int x;
		cin>>x;
		amin[i][0]=amax[i][0]=x;
		if(x>=0) azmin[i][0]=x,afmax[i][0]=LONG_LONG_MIN;
		else azmin[i][0]=LONG_LONG_MAX,afmax[i][0]=x;
	}
	for(int i=1;i<=m;i++) 
	{
		int x;
		cin>>x;
		bmin[i][0]=bmax[i][0]=x;
	}
	for(int i=2;i<=max(n,m);i++) lg[i]=lg[i/2]+1;
	for(int j=1;j<=lg[n];j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
		{
			int p=i+(1<<(j-1));
			amax[i][j]=max(amax[i][j-1],amax[p][j-1]);
			afmax[i][j]=max(afmax[i][j-1],afmax[p][j-1]);
			amin[i][j]=min(amin[i][j-1],amin[p][j-1]);
			azmin[i][j]=min(azmin[i][j-1],azmin[p][j-1]);
		}
	for(int j=1;j<=lg[m];j++)
		for(int i=1;i+(1<<j)-1<=m;i++)
		{
			int p=i+(1<<(j-1));
			bmax[i][j]=max(bmax[i][j-1],bmax[p][j-1]);
			bmin[i][j]=min(bmin[i][j-1],bmin[p][j-1]);
		}
	while(q--)
	{
		int l1,r1,l2,r2;
		cin>>l1>>r1>>l2>>r2;
		int ka=lg[r1-l1+1],kb=lg[r2-l2+1];
		int pa=r1-(1<<ka)+1,pb=r2-(1<<kb)+1;
		ll a_max=max(amax[l1][ka],amax[pa][ka]);
		ll a_min=min(amin[l1][ka],amin[pa][ka]);
		ll af_max=max(afmax[l1][ka],afmax[pa][ka]);
		ll az_min=min(azmin[l1][ka],azmin[pa][ka]);
		ll b_max=max(bmax[l2][kb],bmax[pb][kb]);
		ll b_min=min(bmin[l2][kb],bmin[pb][kb]);
		ll ans=LONG_LONG_MIN;
		ans=max(ans,a_max*(a_max>=0?b_min:b_max));
		ans=max(ans,a_min*(a_min>=0?b_min:b_max));
		if(af_max!=LONG_LONG_MIN) ans=max(ans,af_max*(af_max>=0?b_min:b_max));
		if(az_min!=LONG_LONG_MAX) ans=max(ans,az_min*(az_min>=0?b_min:b_max));
		cout<<ans<<endl;
	}
	return 0;	
} 

T3 星战

先翻译一下题目:
\(n\) 个点 \(m\) 条边的有向图,每条边都有激活和失活两种状态,初始时均为激活状态。四种操作:

失活某条边;
失活以某个点为终点的所有边;
激活某条边;
激活以某个点为终点的所有边。

然后问:如果只考虑激活的边,是否满足:

所有的点出度均为 \(1\)
所有的点都满足,从这个点出发,可以走到一个环中。

首先我们发现,如果所有点的出度均为 \(1\),那么所有点都满足从这个点出发能走到一个环里。这是因为所有点的出度都是 \(1\),因此一条路径可以一直走下去,而只要走 \(n\) 步就一定会遇到先前走过的一个节点(总共只有 \(n\) 个点),此时环就出现了。
因此压根不用判环,只用判断所有点出度是否均为 \(1\)

事实上,所有点出度均为 \(1\) 的有向图称作基环内向森林,它由若干个弱联通块(将所有有向边看成无向边后的联通块)构成,每个弱联通块称作基环内向树。对于基环内向树,它可以被认为是由有向环和长在环上的若干条链构成。这些链单向导通到环上。更多有关定义可以自行阅读 OI Wiki 的图论相关概念。

我们观察到,一、三操作可以用 \(\mathcal{O}(1)\) 的效率修改一个点的出度(修改目标边的终点出度即可),而二、四操作只能用 \(\mathcal{O}(n)\) 的效率(因为终点为 \(v\) 的边有很多,对应的起点最多有 \(n\) 个,它们的出度都需要被修改)。
不过,前 8 个测试点,是支持我们用最坏时间复杂度 \(\mathcal{O}(nq)\) 的暴力维护出度的,所以 40 分已经到手。
然后 9 和 10 两个测试点还是保证没有二、四操作的。单次操作可以做到 \(\mathcal{O}(1)\)。因此这两个测试点用 \(\mathcal{O}(q)\) 的复杂度解决,50 分到手。
这里是我的 50 分代码。

思考许久后我发现,容易维护的不是出度,而是入度。具体来说:
设原图上点 \(u\) 的入度为 \(g(u)\),当前点 \(u\) 入度为 \(r(u)\)

失活 \((u, v)\)\(r(v) \gets r(v) - 1\)
失活以 \(v\) 为终点的所有边:\(r(v) \gets 0\)
激活 \((u, v)\)\(r(v) \gets r(v) + 1\)
激活以 \(v\) 为终点的所有边:\(r(v) \gets g(v)\)

这些都可以 \(\mathcal{O}(1)\) 完成。
那么入度和出度有什么关系呢?
一张图里,所有点的入度和等于出度和。我们的目标是所有出度都是 \(1\),那么所有出度的和都是 \(n\)。因此入度的和也必须是 \(n\)
于是我们得到所有出度均为 \(1\) 的一个必要条件:入度和为 \(n\)。然而这个条件并不充分,因为入度和为 \(n\) 可以推出出度和为 \(n\),但不能再进一步推出所有出度均为 \(1\)
比如,如果有三个点,点 \(1\) 出度为 \(2\),点 \(2\) 出度为 \(1\),点 \(3\) 没有出度,我们如果单纯地去计算它的入度和,发现它等于 \(n\),就判断现在每个点出度都是 \(1\),这明显是错误的。
但是,我们可以利用 哈希 使得这个条件的充分性有极大概率是正确的,进一步使得这个条件的充要性极大概率正确,通过本题。

我们初始给每个点随机一个权值 \(w(u)\)。重新定义,一个点 \(v\) 对应的 \(r(v)\),表示直接连向 \(v\) 的所有 \(u\)\(w(u)\) 之和,即:
\(r(v) = \sum_{(u, v) \in E}w(u)\)\(g(v)\) 代表初始所有边都被激活时的 \(r(v)\) 的值,且之后不改变(静态)。
重新设计:

失活 \((u, v)\)\(r(v) \gets r(v) - w(u)\)
失活以 \(v\) 为终点的所有边:\(r(v) \gets 0\)
激活 \((u, v)\)\(r(v) \gets r(v) + w(u)\)
激活以 \(v\) 为终点的所有边:\(r(v) \gets g(v)\)

这个过程中,所有点的 \(r\) 值之和 \(\sum r(u)\) 可以在每次操作时 \(\mathcal{O}(1)\) 地维护。更进一步地:

\(\{u_1, u_2, \ldots\}\)\(v\) 的入点集合,则 \(r(v) = w(u_1) + w(u_2) + \ldots\)。理由是 \(r(v)\) 的定义。
如果所有点 \(u\) 的出度都是 \(1\),那么 \(u\) 只会出现在它所连向的 \(v\) 的入点集合中。因此,将所有入点集合的权值加起来,即 \(\sum r(u)\),我们应该恰好得到 \(\sum w(u)\)。也即,\(\sum r(u) = \sum w(u)\)

因此,\(\sum r(u) = \sum w(u)\) 是每个点出度都为 \(1\) 的一个必要条件。那么,这个条件充分吗?
一般地,我们设点 \(u\) 的出度为 \(k(u)\),意味着 \(w(u)\) 恰好在最终的 \(\sum r(u)\) 中出现 \(k(u)\) 次。也即 \(\sum r(u) = \sum k(u) \times w(u)\)。因此我们可以将判断条件 \(\sum r(u) = \sum w(u)\) 改写为:\(\sum k(u) \times w(u) = \sum w(u)\)
显然所有 \(k(u) = 1\) 是一个解。有其它的解吗?
有,但是这样的一组 \(k\)\(w\) 高度相关,除非出题人精心构造数据使得目前的局面形成的 \(k\) 存在一个不等于 \(1\) 还恰好满足这个方程,否则你可以认为当这个方程成立的时候,目前的 \(k\) 都等于 \(1\)
又因为你的 \(w\) 是随机的,所以数据是没法对着你的代码卡的,除非你的 \(w\) 正好随机到一个能把你代码卡掉的数据,这种可能太小了基本可以忽略不计,因为随机的值域已经相当大了。
这就是哈希的原理了:判定时构造一个必要不充分条件,但这个“不充分”实际上有非常大非常大的概率充分,以至于不充分性可以忽略不计,从而达到充要判定的效果。读者应当熟悉的字符串哈希,也是同样的原理。
这种哈希似乎一般称作“和哈希”(sum hash)。
代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
typedef long long ll;
ll in[N],a[N],w[N];
int main()
{
	int n,m;
	cin>>n>>m;
	ll s=0;
	for(int i=1;i<=n;i++) w[i]=rand(),s+=w[i];
	ll t=0;
	for(int i=1;i<=m;i++)
	{
		int u,v;
		cin>>u>>v;
		in[v]+=w[u];
		a[v]=in[v];
		t+=w[u];
	}
	int q;
	cin>>q;
	while(q--)
	{
		int op,u,v;
		cin>>op;
		if(op==1) cin>>u>>v,in[v]-=w[u],t-=w[u];
		else if(op==2) cin>>v,t-=in[v],in[v]=0;
		else if(op==3) cin>>u>>v,in[v]+=w[u],t+=w[u];
		else cin>>v,t+=a[v]-in[v],in[v]=a[v];
		if(t==s) cout<<"YES"<<endl;
		else cout<<"NO"<<endl;
	}
	return 0;
}