骗分过样例,暴力出奇迹

2017年上海市计算数学代表队选拔赛

Task 4 treediagram 摧毁“树形图”

你指尖跃动的电光,是我此生不变的信仰,唯我超电磁炮永世长存!

题面

题目描述

自从上次神刀手帮助蚯蚓国增添了上千万人口(蚯口?),蚯蚓国发展得越来越繁荣了!最近,他们在地下发现了一些神奇的纸张,经过仔细研究,居然是 D 国 X 市的超级计算机设计图纸!

这台计算机叫做‘‘树状图’’,由 nn 个计算节点与 n1n-1 条可以双向通信的网线连接而成,所有计算节点用不超过 nn 的正整数编号。顾名思义,这形成了一棵树的结构。蚯蚓国王已在图纸上掌握了这棵树的完整信息,包括 nn 的值与 n1n-1 条网线的连接信息。于是蚯蚓国王决定,派出蚯蚓国最强大的两个黑客,小 P 和小 H ,入侵‘‘树状图’’,尽可能地摧毁它。

  • 小 P 和小 H 精通世界上最好的编程语言,经过一番商量后,他们决定依次采取如下的步骤:
  • 小 P 选择某个计算节点,作为他入侵的起始点,并在该节点上添加一个 P 标记。
  • 重复以下操作若干次(可以是 0 次):小 P 从他当前所在的计算节点出发,选择一条没有被标记过的网线,入侵到该网线的另一端的计算节点,并在路过的网线与目的计算节点上均添加一个 P 标记。
  • 小 H 选择某个计算节点,作为她入侵的起始点,并在该节点上添加一个 H 标记。
  • 重复以下操作若干次(可以是 0 次):小 H 从她当前所在的计算节点出发,选择一条没有被标记过的网线,入侵到该网线的另一端的计算节点,并在路过的网线与目的计算节点上均添加一个 H 标记。(注意,小 H 不能经过带有 P 标记的网线,但是可以经过带有 P 标记的计算节点)
  • 删除所有被标记过的计算节点和网线。
  • 对于剩下的每条网线,如果其一端或两端的计算节点在上一步被删除了,则也删除这条网线。
  • 经过以上操作后,‘‘树状图’’ 会被断开,剩下若干个(可能是 0 个)连通块。为了达到摧毁的目的,蚯蚓国王希望,连通块的个数越多越好。于是他找到了你,希望你能帮他计算这个最多的个数。
  • 小 P 和小 H 非常心急,在你计算方案之前,他们可能就已经算好了最优方案或最优方案的一部分。你能得到一个值 xx
  • x=0x = 0 ,则说明小 P 和小 H 没有算好最优方案,你需要确定他们两个的入侵路线。
  • x=1x = 1 ,则说明小 P 已经算好了某种两人合作的最优方案中,他的入侵路线。他将选择初始点 p0p_0 ,并沿着网线一路入侵到了目标点 p1p_1 ,并且他不会再沿着网线入侵;你只需要确定小 H 的入侵路线。
  • x=2x = 2 ,则说明小 P 和小 H 算好了一种两人合作的最优方案,小P从点 p0p_0 入侵到了 p1p_1 并停下,小H从点 h0h_0 入侵到了 h1h_1 并停下。此时你不需要指挥他们入侵了,只需要计算最后两步删除计算节点与网线后,剩下的连通块个数即可。

输入格式

从文件中读入数据。
每个输入文件包含多个输入数据。输入文件的第一行为两个整数 TTxxTT 表示该文件包含的输入数据个数, xx 的含义见上述。(同一个输入文件的所有数据的 xx 都是相同的)
接下来依次输入每个数据。
每个数据的第一行有若干个整数:

  • x=0x = 0 ,则该行只有一个整数 nn
  • x=1x = 1 ,则该行依次有三个整数 n,p0,p1n, p_0, p_1
  • x=2x = 2 ,则该行依次有五个整数 n,p0,p1,h0,h1n, p_0, p_1, h_0, h_1

保证 $ p_0, p_1, h_0, h_1$ 均为不超过nn的正整数。
每个数据接下来有 n1n-1 行,每行有两个不超过 nn 的正整数,表示这两个编号的计算节点之间有一条网线将其相连。保证输入的是一棵树。
同一行相邻的整数之间用恰好一个空格隔开。
数据文件可能较大,请避免使用过慢的输入输出方法。

输出格式

输出到文件中。对于每个数据,输出一行,表示在给定条件下,剩下连通块的最大个数。

任务

对于整数 kk ,设 $\sum\ n^k $ 为某个输入中,其 TT 个输入数据的 nkn^k 之和。

所有输入数据满足 T  105, n1  5 × 10  5T\ \leq\ 10^5 , \sum\ n^1\ \leq\ 5\ \times\ 10\ ^\ 5请注意初始化的时间复杂度,避免输入大量小数据时超时。

每个测试点的详细数据范围见下表。

如果表中“完全二叉”为Yes,则该输入文件的每个数据满足:网线信息的第 jj 行, (1  j < n)(1\ \leq\ j\ <\ n) 输入的两个数依次是 floor((j+1)/2)floor((j+1)/2)j+1j+1

解法

部分分

骗分过样例,暴力出奇迹。

  1. 考虑 x=2x=2 的情况,直接在图上计算联通块即可。
  2. 考虑 n  7n\ \leq\ 7 ,均可手推得到。
  3. x=2x=2 的情况推广,那么对于 x=1x=1x=0x=0 实际上都可以通过枚举路线得到,时间复杂度分别为 O(N3)O(N^3)O(N5)O(N^5) 。可以获得60分。
  4. 对于树是完全二叉树的情况,显然是有规律的,可以强行找规律,但是比较麻烦。

以上的做法总共可以获得80分。上海最高分相当于此。(虽然只是因为Rk1的正解写挂了。)

伪正解与正解

注意到是树上的最优性问题,模型无法联系点分治,树链剖分等经典模型,只能考虑树形动态规划。由于以点为讨论对象的状态转移方程的情况与边界条件多到离谱,不是很好的解法,我们不单独列出。更好的做法是针对边,对于一根有向边 e:u>ve : u->v ,考虑:

fe=max{degv1,fe1+degv2}(e1:v>w,w u)f_e=max\{deg_v-1,f_e1+deg_v-2\}(e1:v->w,w\neq\ u)

ge=max{fe,fe1+fe2+degv3}(e1:v>w,e2:v>x,w,x,u)互不相等g_e=max\{f_e,f_{e1}+f_{e2}+deg_v-3\}(e1:v->w,e2:v->x,w,x,u互不相等)

h_e=max\{g_e,h_{e1}\}(e1:v->w,w\ \ne\ u)$$。 我们考虑两条路径的合并,如果两条路径不交,我们可以以此更新答案: $$h_{e1}+h_{e2}(e1:u->v,e2:v->u)

he1+he2+1(e1:u>v,e2:u>w,vw)h_{e1}+h_{e2}+1 (e1:u->v,e2:u->w,v\ne w)

如果两条路径在某点相交,可以以此更新答案:

fe1+fe2+fe3+degu3(e1:u>v,e2:u>w,e3:u>x,v,w)f_{e1}+f_{e2}+f_{e3}+deg_u-3(e1:u->v,e2:u->w,e3:u->x,v,w)

fe1+fe2+fe3+fe4+degu4(e1:u>v,e2:u>w,e3:u>x,e4:u>y,v,w,x)f_{e1}+f_{e2}+f_{e3}+f_{e4}+deg_u-4(e1:u->v,e2:u->w,e3:u->x,e4:u->y,v,w,x)

求解用记忆化搜索即可,详情见代码:

#include <stdio.h>
#define max(a,b) ((a)>(b)?a:b)
#include <string.h>
#define INF 10000000
#define MAXN 5*100000+20
int x,n,next[MAXN],head[MAXN],edge[MAXN],h[MAXN],g[MAXN],f[MAXN],deg[MAXN];
bool vis[MAXN];
#define addEdge(num,from,to) (next[num]=head[from],head[from]=num,edge[num]=to)
inline int read(){ 
	int ret=0;char ch=getchar(); 
	while(ch>'9'||ch<'0') ch=getchar(); 
	while(ch>='0'&&ch<='9') {
						ret=ret*10+ch-'0';
						ch=getchar(); 
					} 
	return ret; 
} 
inline void dp(int e){
	if(vis[e]) return ;
	int u=edge[e],f1=-INF,f2=-INF;
	vis[e]=true,h[e]=-INF;
	for(register int i=head[u];i+1;i=next[i]) if(i!=(e^1)){
		dp(i);
		h[e]=max(h[e],g[i]);
		if(f[i]>=f1) f2=f1,f1=f[i];
		else f2=max(f2,f[i]);
	}
	f[e]=max(deg[u]-1,f1+deg[u]-2);
	g[e]=max(f[e],f1+f2+deg[u]-3);
	h[e]=max(h[e],g[e]);
	return ;	
}
void Solve(){
	register int from,to,f1,f2,f3,f4,h1,h2,Ans=0;
	memset(deg,0,sizeof(deg));
	if(!x) n=read();
	else if(x==1) n=read(),read(),read();
	else if(x==2) n=read(),read(),read(),read(),read();
	for(register int i=0;i<2*n;++i) head[i]=next[i]=-1;
	
	for(int i=1;i<n;++i){
		int from=read(),to=read();
		addEdge(i<<1,from,to),addEdge(i<<1|1,to,from);
		++deg[from],++deg[to];
		vis[i<<1]=vis[i<<1|1]=false;
	}
	for(int i=1;i<=n;++i){
		f1=f2=f3=f4=h1=h2=-INF;
		for(int j=head[i];j+1;j=next[j]){
			dp(j);
			Ans=max(Ans,h[j]+1);
			if(vis[j^1]) Ans=max(Ans,h[j]+h[j^1]);
			if(f[j]>=f1) f4=f3,f3=f2,f2=f1,f1=f[j];
			else if(f[j]>=f2) f4=f3,f3=f2,f2=f[j];
			else if(f[j]>=f3) f4=f3,f3=f[j];
			else f4=max(f4,f[j]);
			if(h[j]>=h1) h2=h1,h1=h[j];
			else h2=max(h2,h[j]);
		}
		Ans=max(Ans,h1+h2+1);
		Ans=max(Ans,f1+f2+f3+deg[i]-3);
		Ans=max(Ans,f1+f2+f3+f4+deg[i]-4);
	}
	printf("%d\n",Ans);
}
int main(){
	int T=read();
	x=read();
	while(T--) Solve();
	return 0;
}

但这里有一个要注意的地方,即按这个做法,算法的稳定性非常差。事实上不能称为线性,而依赖于给定的树的形态。遇上度数较大的数据,即“菊花”形态的数据,会强行被卡。这里重新设计状态可以有效提高算法的稳定性。关于这种做法,可以参考参考资料中 sengxian 的做法。

Task 5 trennen 分手是祝愿

题面

问题描述

Zeit und Raum trennen dich und mich.

时空将你我分开。

B君在玩一个游戏,这个游戏由 nn 个灯和 nn 个开关组成,给定这 nn 个灯的初始状态,下标为从 11nn 的正整数。

每个灯有两个状态亮和灭,我们用 1 来表示这个灯是亮的,用 0 表示这个灯是灭的,游戏的目标是使所有灯都灭掉。

但是当操作第 ii 个开关时,所有编号为 ii 的约数(包括 11ii )的灯的状态都会被改变,即从亮变成灭,或者是从灭变成亮。

B君发现这个游戏很难,于是想到了这样的一个策略,每次等概率随机操作一个开关,直到所有灯都灭掉。

这个策略需要的操作次数很多,B君想到这样的一个优化。如果当前局面,可以通过操作小于等于 kk 个开关使所有灯都灭掉,那么他将不再随机,直接选择操作次数最小的操作方法(这个策略显然小于等于 kk 步)操作这些开关。

B君想知道按照这个策略(也就是先随机操作,最后小于等于 kk 步,使用操作次数最小的操作方法)的操作次数的期望。

这个期望可能很大,但是B君发现这个期望乘以 nn 的阶乘一定是整数,所以他只需要知道这个整数对100003100003 取模之后的结果。

输入格式

从文件中读入数据。
第一行两个整数 n,kn, k
接下来一行 nn 个整数,每个整数是 0 或者 1 ,其中第 ii 个整数表示第 ii 个灯的初始情况。

输出格式

输出到文件中。
输出一行,为操作次数的期望乘以 nn 的阶乘对 100003100003 取模之后的结果。

任务

  1. 对于 0%0\% 的测试点,和样例一模一样;
  2. 对于另外 30%30\% 的测试点, n10n \leq 10
  3. 对于另外 20%20\% 的测试点, n100n \leq 100
  4. 对于另外 30%30\% 的测试点, $n \leq 1000 $ ;
  5. 对于 100%100\% 的测试点,1n100000,0kn1 \leq n \leq 100000, 0 \leq k \leq n
  6. 对于以上每部分测试点,均有一半的数据满足 k=nk = n

解法

考虑按一次开关的影响,发现按一次开关,最多只可能使得剩下的按压次数变为 k1k-1 , kk , k+1k+1 ,以这个形式建立概率-期望系统。可以得到方程式:

\begin{eqnarray}f(x)= \begin{cases} i, &i\leq k\cr \frac{1}{n} (if(i-1)+(n-i)f(i+1))+1, &k<i<n \cr f(i-1)+1, &i=n\end{cases} \end{eqnarray}

部分分

  1. 用高斯消元法强解概率-期望系统,可以得到大约 $ 50% $ 的分数。
  2. 对于 n=kn=k ,从大往小选,可以获得 50%50\% 的分数。

正解

对方程式中k<i<nk<i<n的部分进行移项,得到 $ f(i-1)=\frac{1}{i}(nf(i)-(n-i)f(i+1)-n) $。运用之前提到的逆元的知识,即可AC。

#include <stdio.h>
#define MAXN 100000+20
#define mod 100003
#define ll long long
bool a[MAXN],tmp,mark[MAXN];
int n,k,t=0;
ll fac=1,inv[MAXN],g[MAXN];
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;++i) scanf("%d",a+i),fac=fac*(long long)i%mod;
	for(int i=n;i>=1;--i){
		tmp=a[i];
		for(int j=i+i;j<=n;j+=i) if(mark[j]) tmp^=1;
		if(tmp) mark[i]=1,++t;
	}
	if(t<=k) {
		printf("%lld",t*fac%mod);
		return 0;
	}
	inv[1]=1;
	for(int i=2;i<=n;++i) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	for(int i=1;i<=k;++i) g[i]=1;
	g[n]=1;
	for(int i=n-1;i>k;--i) g[i]=((n-i)*g[i+1]%mod+n)*inv[i]%mod;
	ll ans=0;
	for(int i=1;i<=t;++i) ans+=g[i];
	printf("%lld\n",fac*ans%mod);
	return 0;
}

题面

问题描述

Kiana最近喜欢到一家非常美味的寿司餐厅用餐。每天晚上,这家餐厅都会按顺序提供 nn 种寿司,第 ii 种寿司有一个代号 aia_i 和美味度 di,id_{i,i} ,不同种类的寿司有可能使用相同的代号。每种寿司的份数都是无限的,Kiana也可以无限次取寿司来吃,但每种寿司每次只能取一份,且每次取走的寿司必须是按餐厅提供寿司的顺序连续的一段,即Kiana可以一次取走第 1,21, 2 种寿司各一份,也可以一次取走第 2,32, 3 种寿司各一份,但不可以一次取走第 1,31, 3 种寿司。

由于餐厅提供的寿司种类繁多,而不同种类的寿司之间相互会有影响:三文鱼寿司和鱿鱼寿司一起吃或许会很棒,但和水果寿司一起吃就可能会肚子痛。因此,Kiana定义了一个综合美味度 di,j(i<j)d_{i, j} (i < j) ,表示在一次取的寿司中,如果包含了餐厅提供的从第 ii 份到第 jj 份的所有寿司,吃掉这次取的所有寿司后将获得的额外美味度。由于取寿司需要花费一些时间,所以我们认为分两次取来的寿司之间相互不会影响。注意在吃一次取的寿司时,不止一个综合美味度会被累加,比如若Kiana一次取走了第 1,2,31,2,3 种寿
司各一份,除了d1,3d_{1,3} 以外,d1,2d_{1,2}, d2,3d_{2,3}也会被累加进总美味度中。

神奇的是,Kiana的美食评判标准是有记忆性的,无论是单种寿司的美味度,还是多种寿司组合起来的综合美味度,在计入Kiana的总美味度时都只会被累加一次。比如,若Kiana某一次取走了第 1,21,2 种寿司各一份,另一次取走了第2,3种寿司各一份,那么这两次取寿司的总美味度为 d1,1+d2,2+d3,3+d1,2+d2,3d_{1,1} + d_{2,2} + d_{3,3} + d_{1,2} + d_{2,3} ,其中d2,2d_{2,2} 只会计算一次。奇怪的是,这家寿司餐厅的收费标准很不同寻常。具体来说,如果Kiana一共吃过了 cc 种代号为 xx 的寿司,则她需要为这些寿司付出 mx2+cxmx^2 + cx 元钱,其中 mm 是餐厅给出的一个常数。

现在Kiana想知道,在这家餐厅吃寿司,自己能获得的总美味度(包括所有吃掉的单种寿司的美味度和所有被累加的综合美味度)减去花费的总钱数的最大值是多少。由于她不会算,所以希望由你告诉她。

输入格式

从文件中读入数据。
第一行包含两个正整数 n,mn,m ,分别表示这家餐厅提供的寿司总数和计算寿司价格中使用的常数。
第二行包含 nn 个正整数,其中第 kk 个数 aka_k 表示第 kk 份寿司的代号。
接下来 nn 行,第 ii 行包含 ni+1n - i + 1 个整数,其中第 jj 个数 $d_{i,i+j-1} $ 表示吃掉寿司能获得的相应的美味度,具体含义见问题描述。

输出格式

输出到文件中。
输出共一行包含一个正整数,表示Kiana能获得的总美味度减去花费的总钱数的最大值。

任务

解法

部分分

论谁更能骗分。

  1. $ N \leq 10 $ : 状态压缩DP,可以通过前10个测试点

  2. $ m=0 $ 普通的资源分配型DP

    定义部分和sl,r=sl+1,r+dl,rs_{l,r}=s_{l+1,r}+d_{l,r}si,i=di,iais_{i,i}=d_{i,i}-a_if(l,r)f(l,r)为当前决策区间最终能获得的最大得分,可以获得如下递推式:f(l,r)=max{f(l,r+1)+sl,r,if l<r then f(l+1,r) else f(l+1,r+1)}f(l,r)=max\{f(l,r+1)+s_{l,r},if\ l<r\ then\ f(l+1,r)\ else\ f(l+1,r+1) \}。解即为f(1,1)f(1,1),可以得到60分。

正解

考虑最小割中的经典模型“最大权闭合图”。所谓最大权闭合图,即考虑如下的情况:

定义一个有向图 G=(V,E)G=(V,E) 的闭合图是该有向图的一个点集 $ V' $,且该点集的所有出边均指向这个点集,即闭合图内的任意点的后继也一定在这个闭合图中。则给每个点 $ v $ 分配一个点权 $w_v $ ,最大权闭合图即点权之和最大的闭合图,即最大化 $ \sum_{v \in v'} w_v $ 。

那么考虑如何求解。首先在原图上建立源点 $ s $ 和汇点 $ t $ ,将原图每条有向边 $ < u , v > \in E $ 替换为容量为 $ c ( u , v ) = + $ 的有向边 $ < u , v > \in E_N $ ,增加连接源 $ s $ 到原图每个正权点 $ v $ 的有向边 $ < s,v > \in E_N $

,容量为 c(s,v)=wvc(s,v)=w_v,增加连接原图每个负权点vv到汇的有向边 $ < v,t> \in E_N $ ,容量为c(v,t)=wvc(v,t)=-w_v。最大的权值和即为所有点的正权之和减最小割(即最大流)。

最大权闭合子图一般应用在有依赖关系,无约束的决策上,尤其是决策重合程度很高的情况下。如本例即是。这里使用刘汝佳的Dinic算法模板求解最大流。

#include <stdio.h>
#include <string.h>
#include <vector>
#include <queue>
#define min(a,b) (a<b?a:b)
using std::vector;
using std::queue;
#define MAXN 30200+10
#define INF 2147483647
#define MAXM 1000000+10
int n,m,s,t;

struct Edge{
	int from,to,cap,flow;
};
vector <Edge> Edges;
vector <int> G[MAXN];
bool vis[MAXN];
int d[MAXN],cur[MAXN];
bool BFS(){
	memset(vis,0,sizeof(vis));
	queue < int > Q;
	Q.push(s);
	d[s]=0,vis[s]=true;
	while(!Q.empty()){
		int x=Q.front();Q.pop();
		for(int i=0;i<G[x].size();++i){
			Edge &e=Edges[G[x][i]];
			if(!vis[e.to]&&e.cap>e.flow){
				vis[e.to]=true;
				d[e.to]=d[x]+1;
				Q.push(e.to);
			}
		}
	}
	return vis[t];
}
int DFS(int x,int a){
	if(x==t||a==0) return a;
	int flow=0,f;
	for(int &i=cur[x];i<G[x].size();++i){
		Edge &e=Edges[G[x][i]];
		if(d[x]+1==d[e.to]&&(f=DFS(e.to,min(a,e.cap-e.flow)))>0){
			e.flow+=f,flow+=f,a-=f;
			Edges[G[x][i]^1].flow-=f;
			if(!a) break;
		}
	}
	return flow;
}
int MaxFlow(){
	int flow=0;
	while(BFS()){
		memset(cur,0,sizeof(cur));
		flow+=DFS(s,INF);
	}
	return flow;
}
void addEdge(int x,int y,int c){
	Edges.push_back((Edge){x,y,c,0});
	Edges.push_back((Edge){y,x,0,0});
	int m=Edges.size();
	G[x].push_back(m-2);
	G[y].push_back(m-1);
	return ;
}
int a[1010],id[110][110],map[110][110],sum=0,ict,idw[MAXN];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",a+i);
	for(int i=1;i<=n;++i)
		for(int j=i;j<=n;++j)
			scanf("%d",&map[i][j]);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			id[i][j]=++ict;
	for(int i=1;i<=n;++i)
		if(!vis[a[i]]){
			vis[a[i]]=1;
			idw[a[i]]=++ict;
		}
	s=0,t=ict+n+1;
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;++i) if(!vis[a[i]]) vis[a[i]]=true,addEdge(idw[a[i]],t,m*a[i]*a[i]);
	for(int i=1;i<=n;++i) addEdge(ict+i,idw[a[i]],INF),addEdge(ict+i,t,a[i]);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j){
			if(map[i][j]>0) {
				sum+=map[i][j];
				addEdge(s,id[i][j],map[i][j]);
				for(int k=i;k<=j;++k) addEdge(id[i][j],ict+k,INF);
			}
			else if(map[i][j]<0){
				addEdge(id[i][j],t,-map[i][j]);
				for(int k=i;k<=j;++k) addEdge(id[i][j],ict+k,INF);
			}
			if(i!=j)
				addEdge(id[i][j],id[i+1][j],INF),addEdge(id[i][j],id[i][j-1],INF);
		}
	printf("%d\n",sum-MaxFlow());
}

反思

今后做题要远远超出骗分的视界,也要超出正解的视界。知识体系必须更加完整,以应付尤其是数论,数据结构与网络流为主的命题方向。同时应更加注重日常练习。

参考资料(排名不分先后)

  1. 2007国家集训队论文 胡伯涛 《最小割模型在信息学竞赛中的应用》
  2. 2009国家集训队论文 汤可因 《浅析竞赛中一类数学期望问题的解决方法》
  3. 郭晓旭 杨宽《线性递推关系与矩阵乘法》
  4. SHTSC2017 试题与官方题解
  5. D2T1严格线性做法:https://blog.sengxian.com/solutions/bzoj-4871
  6. 简明数论 北京大学出版社
  7. http://www.lydsy.com/JudgeOnline/problem.php?id=3884 - PoPoQQQ的经典题目,相信是 D2T1 的灵感来源之一。

Acknowledgement

  • 感谢吴申广特派员和清华算协的努力,使 SHTSC2017 不算完满的结束了。
  • 祝贺上海进入省队的九位选手。
  • 感谢上海信息竞赛界的成员们过去在技术上,在精神上的的帮助和支持。尤其是我校两校区的竞赛
  • 感谢 WC2017 冬眠营& Universal OJ 交流群的神犇们的指导。
  • 感谢大家能不嫌弃我,看完这一篇解题报告。