Constructive Algorithms!

Constructive Algorithms!#

前言#

构造题,顾名思义,就是有 constructive algorithms 标签的题目。它们往往给出某些若干限制,并要求选手构造出符合这些限制的对象,例如序列、图和树等。当然,对于某些交互题,构造具体的询问策略也可以视作一类构造题。甚至,一道带输出方案的数据结构题也可能是构造题。这类题目往往不好用某一类特定算法来解决,只能对具体问题具体分析。

然而,尽管题与题之间最终算法 / 构造出的方案差距很大,这类题目还是有一些基本的技法和分析技巧。本篇挑选了一些有代表性的题目,旨在展示面对在各种风格迥异的题目的时候,一些可能有效的分析方法,以及一些可能可参考的,逐步得到可行解的思考路径。

可能有用的方法#

如果直接在方法处插入对应例题就有点奇怪:这样提示就太明显了。

所以这一节是一些不结合题目的纸上谈兵。请仔细体会。

方法零:简单尝试#

很多选手的注意力相当集中。他们往往可以直接观察到一组解,然后通过;或是尝试一些简单策略,然后通过。如果一个题短时间内通过了非常多人,那么就可以试试枚举简单策略。

方法一:精细构造#

对于一些题目,构造的限制可能比较严格。但是,我们可以相对容易地得到离限制较为接近的一组解。这个时候,重新审视构造这组解的过程,我们可能发现某一些部分可以更优,从而,对这一组解微调就可以得到满足限制的解。

例如:

  • 递归到小范围的时候,可以直接使用搜索得出的最优解,而不是人工构造的可行解;
  • 交互题的询问进行到某个阶段的时候,可以复用一部分之前得出的信息;
  • 构造进行到某个阶段的时候,当前局面的性质已经比初始时更好,从而可以换用常数更小的操作方法继续构造。

方法二:打表 (小范围搜索)#

对于限制越少的题目,打表的用武之地就越大。一般来说,题目的输入量越少(例如只有一个 n),构造的自由度就越大。这个时候,解的组数会很多,最优解 / 最小字典序解通常也会具有某种规律。观察出这些规律,就有较大概率通过题目。

例如:

  • 直接搜索得出 n 较小时的一组解,然后推广到一般情况;
  • 搜索得出有解的情况,只针对有解情况进行人工构造,避免虚空做题;
  • 搜索一些特殊局面下的解,试图获得启发,然后推广到一般情况。

当然,打表并非要依赖输入量少这一性质。部分题目中,尽管输入规模可能达到 O(n) 级别,但是稍加分析,可以发现,大部分输入都不如特定的某组极限数据强。这个时候,如果能打表得出极限数据的规律,那么稍加微调自然可以解决更弱的输入。

然而,打表也不是万能的。有的时候简单的可行解不是最优解 / 最小字典序解,而是混在解集中,难以辨认。另外,对于部分题目,随着 n 的增长,搜索范围可能会迅速增大(譬如:搜索的时间复杂度是 O((n!)2) 或者 O(2n2)),从而使得搜索运行时间无法接受。

方法三:从解的形式出发#

这里包含两类方法:

  1. 考虑有解的条件。

    对于可能会区分有 / 无解的题目,如果暂且不明确哪些情况会无解,可以先思考一些无解的充分条件。这些充分条件可以排除掉一些情况,使得关于剩下的情况的构造更为简明。如果能证明剩下的情况都可以被构造出来那么就解决了。

    例如:n 是奇 / 偶数的时候无解,由此延伸出各种处理方法:将 (2i1,2i) 配对构造,或者 (i,i+n/2) 配对构造,等等。

  2. 考虑解具体是什么。

    一类最优化问题通常是,让你构造出一组最优解,但是这个最优值具体是多少还不明了。可以考虑先找出这个最优值,再根据这个最优值的形式进行具体构造。

方法四:强化题目限制#

和方法一中那些需要精细构造的题目不同,很多时候题目的自由度太高,解集太大,反而导致无从下手。我们可以自行强化题目限制,缩小解集大小,从而可以结合上人工构造 / 打表等一系列操作。

涉及到不同对象的时候,强化的方法也有区别。这里没有什么好的概括。

方法五:归纳构造#

没什么好的办法的时候可以试试归纳。这样只需要考虑如何从 nn+knnk,比直接构造整个解要好一些。

当然有的时候肯定不能满足于 nn+k,还要结合上 nkn 的操作(通常出现在若干次 O(logn) 次构造出 n 这样的题目里面)。

例如:

  • 给当前局面设计一个势能,然后找到一组操作,使得在操作之后这个局面的势能减少。只要对于每个局面都设计这样一组操作就可以了。
  • 人工构造 / 搜索出小范围的解,然后找到若干组支持 nn+knkn)的操作。组合这些操作得到答案。

方法六:构造基本操作#

有的时候,题目中给出的可用的操作非常奇怪,不符合我们的认知习惯。这个时候可以尝试组合题中的可用操作,得到符合我们认知习惯的基本操作,然后在擅长的领域内解决问题。

例如:组合操作得出循环移位 / 交换 / 给某个位置减少 1,然后利用组合后的操作解决问题。

方法七:随机化 / 乱搞#

实在做不出来的时候也可以试试这个,构造的自由度越高,效果越好,有的时候可能有奇效。

Part 0. 热身题#

这一部分题目不需要很多前面提到的方法。在一定时间的尝试之后我们便可以得到一组看上去正确的解,对这组解微调就可以通过。当然,微调也许是困难的。

AGC052A Long Common Subsequence#

题解

我们发现构造出长度等于 2n 的 LCS 非常简单,可以猜测长度为 2n+1 的 LCS 也不太困难。

一种合理的猜测是,感觉构造和这个 3 有点关系,因为 223=1,说不定只给出 3 个串就是干这个用的呢?

但事实上没有关系,因为我们是有通解的。简单枚举之后发现:输出 n1,然后 n0,然后一个 1 即可通过。这也没什么办法,构造总是需要多多尝试。

证明:若 S 中最后一个位置是 1,那么在 S+S 的前 n 个字符中取出 n1,中间 n1 个字符中取出 n0,最后一个字符固定为 1;否则,设 S 中极长全 0 后缀长度为 k,那么在 S+S 的前 n 个字符中取出 n1k0;而我们只需要在后 n 个字符中取出前 nk0,因为这是极长连续段,所以取完第 nk0 之后后面必然还剩下至少一个 1

Codeforces 1227G Not Same#

题解

题目可以转化为:在 (n+1)×n 的网格内填 0/1,使得第 i 列的和恰好为 ai,且每一行互不相同。

考虑 ai 全部为 n 的时候我们有简单策略,也就是:第 i 列从第 i 行开始填 n1,填出去了就从第 1 行开始继续填。

简单尝试发现:当 ai 降序排序的时候,采用类似的方法,第 i 列从第 i 行开始填 ai1

证明留作习题。

习题解答 反证法。

不妨设第 i 行和第 j(i<j) 完全相同。

如果 jn,考虑 (i,i+1) 的值,它必须为 0,因为 ai+1n。根据假设,(j,i+1) 也必须为 0,那么就导出 ai+1ji1。因为 ai+2ai+1,从而 (i,i+2)(j,i+2) 必须全部为 0,这样就得出了 ai+2ji2。反复运用会得到 aj1,而按照填数规则和假设,(j,j)(i,j) 必须全部为 1,这导出 aj2,从而矛盾。

如果 j=n+1,我们容易得出 a1<i。运用类似的分析,同样容易推出矛盾。

代码
# include <bits/stdc++.h>

const int N=1010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,a[N];
int p[N];
int v[N][N];

inline int cmp(int x,int y){
	return a[x]>a[y];
}

int main(void){
	n=read();
	for(int i=1;i<=n;++i) a[i]=read(),p[i]=i;
	std::sort(p+1,p+1+n,cmp);
	for(int l=1;l<=n;++l){
			for(int d=1,x=l;d<=a[p[l]];++d,x=x%(n+1)+1){
				v[x][p[l]]=1;
			}
	}
	printf("%d\n",n+1);
	for(int i=1;i<=n+1;++i,puts("")) for(int j=1;j<=n;++j) printf("%d",v[i][j]);
	return 0;
}
 

AGC030C Coloring Torus#

题解

题意中给出的四个位置即为该位置行列循环意义下的邻居。

k500 的时候直接第 i 列填 i 即可通过。注意到 k1000,这启发我们构造不会太复杂,可以直接在这种填法上面微调。

简单尝试后发现,某个位置的邻居在斜线上是相邻的。这启发我们把构造旋转 45 度,改为第 i 条对角线填上 i。我们可以很自然地发现第 i 条对角线交错填 ii+500 是可行的,于是我们解决了 k=1000 的情形。对于 500<k<1000 的情况是类似的,具体来说,如果 i+500k 就交错填 ii+500,否则直接填 i 即可。

代码
# include <bits/stdc++.h>

const int N=505,INF=0x3f3f3f3f,V=500;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int k;
int v[N][N];

int main(void){
	k=read();
	if(k<=V){
		printf("%d\n",k);
		for(int i=1;i<=k;++i) for(int j=1;j<=k;++j){
			printf("%d%c",i," \n"[j==k]);
		}
		return 0;
	}else{
		int n=V;
		printf("%d\n",n),k-=n;
		for(int i=1;i<=n;++i){
			for(int x=1,y=i;x<=n;++x,y=(y==1?n:y-1)){
				v[x][y]=(i<=k?(x&1)*n+i:i);
			}
		}
		for(int i=1;i<=n;++i){
			for(int j=1;j<=n;++j) printf("%d ",v[i][j]);
			puts("");
		}
	}
	return 0;
}

Codeforces 1896G Pepe Racing#

题解

本题乍一看无从下手。不妨考虑子问题:如何询问得出这 n2 个数的最大值编号?

我们可以用 n+1 次询问得到答案。具体地,我们给这 n2 个人分为 n 组,查询组内的最大值编号,然后查询这 n 组的最大值编号。

现在,考虑下一个问题:如何询问得出次大值编号?

虽然最大值所在组已经不足 n 人,但是,我们还是可以用 n 次询问覆盖到所有的 n21 个人,最后再查询一次。以此类推,可以发现,总查询次数为惊人的 O(n3)

我们发现,询问次大值时,对于之前信息的利用不够。只有最大值所在组的最大值编号会改变。而对于其它组,最大值编号是不变的。现在问题在于如何求出最大值所在组的最大值编号。

一个直接的想法是,直接拼一个其它组的元素。这是不可行的,就算该元素在自己的组内比较小,也有可能直接比最大值所在组的剩下 n1 个元素都大。但是,当我们重新审视「分组」这一操作的时候,我们发现,这一轮的分组并不需要和上一轮保持一致。因此,我们可以采用以下策略:直接拼一个其它组的,不是所在组最大值的元素到当前组,然后查询。如果该元素是最大值,就直接将这个元素划分到当前组,并成为当前组的最大值。

这一流程显然是可以继续推广的。当剩余元素数量大于 2n1 的时候,我们采用以下策略:

  • 将每个组的最大值编号拼到一次查询中,得到目前的最大值编号。
  • 从最大值编号所在组中删除该元素。此时,该组可能会被删空。因为剩余元素数量大于 2n1,因此至少有 n 个元素没有成为所在组的最大值。我们总是可以将这些元素的若干个,和该组剩余元素,拼成一次询问,然后重新得到该组的最大值。这一过程可能会更改元素的分组。

这样,任意时刻每组都有至少一个元素。

当剩余元素数量等于 2n1 的时候,情况有一些不一样:现在在执行上面策略的第二步时,凑不齐 n 个元素进行一组询问了。我们注意到,现在只剩下 n1 个没有成为所在组最大值的元素,但是总共也只剩下 2n1 个元素。合理猜测剩下的 n1 个元素恰好是前 n1 小。这也是容易证明的:

  • 当一个元素成为组内最大值的时候,它必然不是前 n1 小。
  • 现在剩下 2n1 个元素,我们已知其中 n 个元素不是前 n1 小,那么剩下的 n1 个元素必然是前 n1 小。

于是这一部分可以采用以下方法解决:

  • 如果只剩下一个不是前 n1 小的元素,则它是第 n 小,不需要任何询问。
  • 否则,取出场上所有不是前 n1 小的元素,然后拼上若干个前 n1 小组成一次询问,询问的答案就是剩余元素最大值。然后删去该元素。

考虑总询问次数:

  • 询问最大值:共 n+1 次。
  • 询问中间的 (n21)(2n1) 个数:共 2(n22n) 次。
  • 询问最后 n 个数:共 n 次。

这样,我们的总询问次数恰好为 2n22n+1 次,可以通过此题。

代码
# include <bits/stdc++.h>
const int N=405,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
int a[N];
int n;
typedef std::vector <int> poly;

inline int query(poly &c){
	assert((int)c.size()==n);
	printf("? "); for(auto v:c) printf("%d ",v); puts(""); fflush(stdout);
	return read();
}

std::set <int> mn,s[N];
int bel[N],mx[N];
std::vector <int> ans;

inline void remove(int x){
	ans.push_back(x);
	for(int i=1;i<=n;++i) if(i!=bel[x]&&s[i].size()){
		int v=*s[i].begin();
		s[bel[x]].insert(v),bel[v]=bel[x],s[i].erase(v);
		break;
	}
	std::vector <int> b;
	for(auto v:s[bel[x]]) b.push_back(v);
	if((int)b.size()<n) for(auto v:mn){
		if(bel[v]!=bel[x]) b.push_back(v);
		if((int)b.size()==n) break;
	}
	int id=query(b);
	mx[bel[x]]=id;
	if(bel[id]!=bel[x]) s[bel[id]].erase(id),s[bel[id]=bel[x]].insert(id);	
	mn.erase(mx[bel[x]]),s[bel[x]].erase(mx[bel[x]]);
	return;
}

std::mt19937 rng(time(0));

inline void solve(void){
	n=read(); mn.clear(),ans.clear();
	poly b;
	for(int k=1;k<=n;++k){
		s[k].clear();
		poly w;
		for(int x=(k-1)*n+1;x<=k*n;++x) w.push_back(x),bel[x]=k;
		b.push_back(mx[k]=query(w));
		for(int x=(k-1)*n+1;x<=k*n;++x) if(x!=mx[k]) mn.insert(x),s[k].insert(x);
	}
	remove(query(b));	
	for(int x=n*n-1;x>2*n-1;--x){
		poly w;
		for(int k=1;k<=n;++k) w.push_back(mx[k]);
		remove(query(w));
	}	
	std::set <int> r;
	for(int k=1;k<=n;++k) r.insert(mx[k]);
	while(r.size()>1){
		poly w; for(auto v:r) w.push_back(v);
		

		if((int)w.size()<n) for(auto v:mn){
			w.push_back(v); if((int)w.size()==n) break;
		}
		int cmx=query(w); r.erase(cmx), ans.push_back(cmx);
	}
	ans.push_back(*r.begin());
	printf("! ");
	for(auto v:ans) printf("%d ",v); puts(""); fflush(stdout);	
	return;

}

int main(void){
	int T=read();
	while(T--) solve();
	
	

	return 0;

}

Part 1. 从解的形式出发#

这一部分的题一般要先考虑解的形式,再具体构造。

QOJ 5434 Binary Substrings#

题解

(From Sol1)

枚举每一个长度 l,长度为 l 的本质不同子串个数有两个上界:2lnl+1

直接加起来可以得到答案的显然上界:

i=1nmin{2l,nl+1}

当然,具体构造仍然是不太简单的。

我们感性理解,如果长度为 k01 串出现了 2k 个,那么长度为 k<k01 串肯定也全出现了。同时,如果长度为 k 的所有 nk+1 个子串都是不同的,那么长度为 k>k01 串肯定也全部不同。

找到最大的 k 使得 2knk+1,此时,如果长度为 k01 串全部出现过,长度为 k+1 的子串都是不同的,那么每一部分都取满上界。

考虑图论模型。构造有 2k 个点的图 Gk。图上的所有点恰好代表每一种长度为 k01 串。若串 s 去掉首字符,并加上串 t 的末字符恰好就是串 t,则连边 st。不难发现,每个点有两条入边两条出边,图中共 2k+1 条边,恰好代表每一种长度为 k+101 串。

若能得出图 Gk 的哈密顿回路,则在 Gk 中删掉这些边后,剩下的图中每个点恰为一条出边一条入边,形成若干个环。

注意到哈密顿回路必然和任意环点有交。我们扭转交点处的两条出边,即可将该环插入到哈密顿回路所在的环中。我们添加若干个环直到边数不小于 n(k+1)+1=nk。此时容易构造一条路径使得经过了哈密顿回路上的所有边,且总长度恰好为 nk。不难验证此时同时满足了关于 kk+1 的两条限制。

关于图 Gk 的哈密顿回路求法:

  • 直接搜索。证明留做习题。
  • 注意到图 Gk 的哈密顿回路是图 Gk1 的欧拉回路。跑欧拉回路即可。
代码
# include <bits/stdc++.h>

const int N=1000010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,k;
int b[N],bc;
bool vis[N];
int nex[N];

void dfs(int x){
	if(vis[x]) return;
	vis[x]=true,dfs((2*x)%(1<<k)),dfs((2*x+1)%(1<<k));
	b[++bc]=x;
}

int main(void){
	n=read();
	for(;(1<<k)+(k-1)<=n;) ++k; --k;
	dfs(0); std::reverse(b+1,b+1+bc);
	if(n==(1<<k)+k-1){
		for(int i=1;i<k;++i) putchar('0');
		for(int i=1;i<=bc;++i) putchar('0'+(b[i]&1));
		return 0;
	}
	memset(vis,false,sizeof(vis));
	memset(nex,-1,sizeof(nex));
	for(int i=1;i<=bc;++i) b[i]=(b[i]<<1|(b[i+1]&1)),vis[b[i]]=true;
	

	for(int i=1;i<=bc;++i) nex[b[i]]=b[i%bc+1];
	for(int i=0;i<(1<<(k+1));++i) if(nex[i]==-1) nex[i]=nex[i^(1<<k)]^1;
	
	for(int i=0;i<(1<<(k+1));++i){
		if(vis[i]) continue;
		for(int j=i;!vis[j];j=nex[j]) vis[j]=true,++bc;
		std::swap(nex[i],nex[i^(1<<k)]);
		if(bc+k>=n){
			for(int j=k;j>=0;--j) putchar('0'+((nex[i]>>j)&1));
			for(int j=k+2,p=nex[nex[i]];j<=n;++j,p=nex[p]) putchar('0'+(p&1));
			exit(0);
		}
	}
	return 0;

}

AGC046E Permutation Cover#

题解

何时无解?感性理解,一个元素出现太多或者太少都是不好的。考虑 k=2 的情形,显然必须有 max(a1,a2)2min(a1,a2),最优的排布方法肯定是 {2,1,2,2,1,2,2,1,2} 这样。如果一个 2 到上一个 2 和下一个 2 的中间都没有 1 那肯定不合法。

拓展到 k 更大的情形,设 ax,ay 分别是最小值和最大值。不难发现 ay2ax 这一条件仍然是必要的。下面证明这一条件充分:

  • 按照每次在序列末尾追加元素的方式构造答案。
  • 如果 ax=ay,则追加 1,2,,k,并将所有 k 减去 1
  • 否则,将 {1,2,3,,k} 划分为两个集合 S,T,其中若 ai=ax 则划分到集合 S,否则划分到集合 T。按照 T,S,T 的顺序追加,并将 ai(iS) 减去 1aj(jT) 减去 2

接下来仍然有难点:求字典序最小解。

枚举下一次覆盖结束点,即:枚举 l=1,2,,k,并检查是否可以往当前序列末端添加一个长度为 l 的序列,使得添加完之后,新序列的末尾 k 个恰好是一个排列,同时保证接下来会有解。那么求出字典序最小的合法序列作为这一步添加的序列即可。特殊地,第一次只枚举 l=k

因为我们钦定了新序列的末尾 k 个是一个排列,所以当 l 固定的时候,要添加的元素集合是固定的。考察该集合是否能以任意顺序排列:

  • 求出新的 a 数组,记作 b。类似地定义 bx,by

  • 如果 by2bx,那么接下来一定有解。因此可以将该集合从小到大排列,以保证字典序最小;

  • 如果 by>2bx+1,那么根据和上面类似的证明,无论以什么顺序排布,接下来都一定无解;

  • 如果 by=2bx+1,接下来是有解的!这和上面的情况不同。注意到,如果只有 1,2,3,3,3 这五个元素,不管怎么排布都是无解的。但是如果前面的序列末尾是 [1,2],我们就可以把这五个元素安排成 [3,3,1,2,3]。第一个 3 虽然没办法和后面的元素组成排列,但是可以和前面的序列末尾组成排列 [1,2,3]

    所以这种情况下只是需要精心安排位置。观察发现,我们在后面的构造中,让出现次数等于 by 的元素能够「蹭到」该集合里面出现次数等于 bx 的元素,于是该集合在排列的时候不能把出现次数等于 by 的元素排在出现次数等于 bx 的元素后面。除此之外没有限制。据此贪心放置即可。

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int k,n;
typedef std::vector <int> poly;
poly a;

poly p,cur;

inline int getmx(const poly &a){
	assert(a.size()>=k+1);
	int mx=0;
	for(int i=1;i<=k;++i) mx=std::max(mx,a[i]);
	return mx;
}
inline int getmn(const poly &a){
	assert(a.size()>=k+1);
	int mn=INF;
	for(int i=1;i<=k;++i) mn=std::min(mn,a[i]);
	return mn;
}

inline void calc(int l){
	poly b=a; poly vis(k+1,0);
	for(int i=1;i<=k-l;++i) vis[p[p.size()-i]]=1;
	for(int i=1;i<=k;++i) if(!vis[i]) --b[i];
	int mxv=getmx(b),mnv=getmn(b);	
	if(mnv*2+1<mxv) return;
	

	poly q;
	
	if(mnv*2>=mxv){
		for(int i=1;i<=k;++i) if(!vis[i]) q.push_back(i);
		return cur=std::min(cur,q),void();
	}
	
	int pos=0;
	for(int i=1;i<=k;++i) if(!vis[i]&&b[i]==mxv) pos=i;
	for(int i=1;i<=k;++i){
		if(vis[i]) continue;
		if(i>pos||b[i]!=mnv) q.push_back(i);
		if(i==pos){
			for(int j=1;j<=i;++j) if(!vis[j]&&b[j]==mnv) q.push_back(j);
		}
	}
	return cur=std::min(cur,q),void();

}

int main(void){
	k=read(),a=poly(k+1,0);
	for(int i=1;i<=k;++i) a[i]=read(),n+=a[i];
	if(getmn(a)*2<getmx(a)) puts("-1"),exit(0);
	

	while(p.size()<n){
		cur=poly(1,INF);		
		for(int l=(!p.size())?k:1;l+p.size()<=n&&l<=k;++l) calc(l);		
		for(auto v:cur) p.push_back(v),--a[v];
	}
	for(auto v:p) printf("%d ",v);
	
	return 0;

}

Gym103446L Three,Three,Three#

题解

仍然要先考虑无解的条件。感受一下,发现 m=32n,输出是 m/3 行,这个数字恰好是 n/2。猜一下发现有解的充要条件是:图存在完美匹配。

考虑证明。

  • 如果图存在完美匹配,则删去所有匹配边,图中每个点度数变为 2,即图被分为若干个环。对环随意定向,并认为一条边被这条边的终点管辖。那么,找出每个匹配 (u,v),则依次经过 u 的管辖边,(u,v)v 的管辖边可以找到一条长度为 3 的路径。
  • 如果存在解,那么取出每条路径中间两个点匹配,得到的必然是图的一组完美匹配。因为这一步中如果一个点被多次取出则度数至少为 4

因此,选用一种一般图最大匹配算法即可构造出答案。此处为随机化算法。

代码
# include <bits/stdc++.h>

const int N=1505,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

std::mt19937 rng(114514);

int n,m;
bool vis[N];
std::vector <std::array <int,2> > G[N];
int mat[N];

int _u[N],_v[N];
bool del[N];

bool match(int x){	
	if(G[x].size()>1) for(int _=1;_<=5;++_)
		std::swap(G[x][rng()%G[x].size()],G[x][rng()%G[x].size()]);
	vis[x]=true;
	for(int i=0;i<(int)G[x].size();++i){
		int y=G[x][i][0];
		if(vis[y]) continue; vis[y]=true;
		if(!mat[y]||match(mat[y])){
			mat[x]=y,mat[y]=x;
			return true;
		}
	}
	return false;
}

int p[N];
int ans[N],siz;
int to[N];

inline void solve(void){
	for(int i=1;i<=n;++i) p[i]=i; std::shuffle(p+1,p+1+n,rng);
	for(int i=1;i<=n;++i) std::shuffle(G[i].begin(),G[i].end(),rng);
	memset(mat,0,sizeof(mat));
	int cur=0;
	for(int i=1;i<=n;++i){
		if(!mat[p[i]]){
			for(int _=1;_<=5;++_){
				memset(vis,false,sizeof(vis));
				if(match(p[i])) {++cur; break;}
			}
		}
	}
	if(siz<cur) siz=cur,memcpy(ans,mat,sizeof(ans));
	return;
}

int main(void){
	n=read(),m=read();
	for(int i=1;i<=m;++i){
		int u=read(),v=read(); G[u].push_back({v,i}),G[v].push_back({u,i}); _u[i]=u,_v[i]=v;
	}	
	

	for(int T=1;T<=10;++T) solve();
	
	if(siz!=n/2) printf("IMPOSSIBLE"),exit(0);
	for(int i=1;i<=n;++i){
	    if(i<mat[i]){
	        for(int j=1;j<=m;++j){
	            if(!del[j]&&(i==_u[j]&&mat[i]==_v[j]||i==_v[j]&&mat[i]==_u[j])){
	                del[j]=true; break;
	            }
	        }
	    }
	}
	
	memset(vis,false,sizeof(vis));
	for(int i=1;i<=n;++i){
	    if(vis[i]) continue;
	    for(int x=i;;){
	        int y=0; for(auto e:G[x]) if(!del[e[1]]) {y=e[0]; del[e[1]]=true; break;}
	        assert(y); to[y]=x; vis[x]=true; if(y==i) break;
	        x=y;
	    }
	}
	for(int i=1;i<=n;++i) if(mat[i]<i) printf("%d %d %d %d\n",to[i],i,mat[i],to[mat[i]]);
	
	return 0;

}

AT_hitachi2020_e Odd Sum Rectangles#

题解

转化为 2n2m 列的二维前缀异或和数组 s。矩形 (u,d]×(l,r] 的值的奇偶性等于 sd,rsd,lsu,rsu,l,即选取两行两列的四个位置来异或。

根据均值不等式,那么我们希望两行异或得到的 2m 列中,为 0/1 的列数量尽可能接近。

si,j=popcount(i&j)&1 即可使得两行异或得到的为 0/1 的列数量完全相同。

代码
# include <bits/stdc++.h>

const int N=2005,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,m;
int a[N][N];

int main(void){
	n=(1<<read())-1,m=(1<<read())-1;
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) a[i][j]=(__builtin_popcount(i&j)&1);
	for(int i=n;i;--i) for(int j=m;j;--j) a[i][j]^=(a[i][j-1]^a[i-1][j]^a[i-1][j-1]);
	for(int i=1;i<=n;++i,puts("")){
		for(int j=1;j<=m;++j) printf("%d",a[i][j]);
	}
	
	return 0;
}

Part 2: 打表 (小范围搜索)#

打表过题也算过吗?

Codeforces 1906L Palindromic Parentheses#

题解

打表发现,字典序最小解有好的规律。这里不加描述地直接给出代码,大家也可以尝试自己打个表。

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,k;
int ans[N];

int main(void){
	n=read(),k=read();
	if(k<n/2||k==n) puts("-1"),exit(0);
	
	if(n%4==0){
		for(int i=k+1;i<=n;++i) ans[i]=1;
		for(int i=1;i<=(k-n/2)/2;++i) ans[2*i]=ans[k+1-2*i]=1;
		if(k%2) ans[(k+1)/2]=1;
	}else{
		for(int i=k+1;i<=n;++i) ans[i]=1;
		for(int i=1;i<=(k-n/2)/2;++i) ans[2*i]=ans[k+1-2*i]=1;
		if(k%2==0) ans[(k+1-n/2)]=1;		
	}
	
	for(int i=1;i<=n;++i) putchar("()"[ans[i]]);
	
	return 0;
}

CCPC Online A 军训 I#

题解

操作太多次肯定和操作 O(1) 次等价。进一步发现本质不同的操作只有:

  • 什么都不做;
  • 做一次操作:U,D,L,R;
  • 做两次操作:UL,UR,DL,DR,LU,LD,RU,RD。

更长的操作序列都可以简化。对于出现了多个 U/D,L/R 的情况,和只保留最后一次出现是等价的。据此得到 k>13 全部无解。利用单次 O(k2nmpoly(nm)) 的暴力运行若干组 n,m 较小的数据后寻找规律即可。

代码 (by H_W_Y)
#include <bits/stdc++.h>
using namespace std;

const int N=1e3+5;
int n,m,mp[N][N],k;
bool fl;

int gcd(int a,int b){return !b?a:gcd(b,a%b);}

void NO(){cout<<"No\n";}

void YES(){
  cout<<"Yes\n";

  if(!fl){
	for(int i=1;i<=n;i++){
	  for(int j=1;j<=m;j++) cout<<(mp[i][j]?'*':'-');
	  cout<<'\n';
	}
  }else{
	for(int j=1;j<=m;j++){
	  for(int i=1;i<=n;i++) cout<<(mp[i][j]?'*':'-');
	  cout<<'\n';
	}
  }
}

void DO1(){
  for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) mp[i][j]=1;
  YES();
}

void DO2(){
  for(int i=1;i<=n;i++) mp[i][1]=1;
  YES();
}

void DO3(){
  if(m<=2) return NO();
  for(int i=1;i<=n;i++) mp[i][2]=1;
  YES();
}

void DO5(){
  if(gcd(n,m)==1) return NO();
  int d=gcd(n,m);
  for(int c=1;c<=d;c++){
	for(int i=(c-1)*(n/d)+1;i<=c*(n/d);i++)
	  for(int j=(c-1)*(m/d)+1;j<=c*(m/d);j++)
	    mp[i][j]=1;
  }
  YES();
}

void DO4(){
  mp[1][1]=1;
  YES();
}

void DO6(){
  mp[1][2]=1;
  YES();
}

void DO7(){
  if(n==2) mp[1][2]=mp[2][1]=1;
  else{
	for(int i=2;i<=m;i++) mp[1][i]=1;
	mp[2][1]=1;
  }
  YES();
}

void DO9(){
  if(n==2){
    mp[2][1]=mp[1][3]=1;
  }else mp[2][2]=1;
  YES();
}

void DO11(){
  if(n==2){
    if(m<=3) return NO();
	mp[2][1]=mp[1][2]=mp[1][4]=1;
  }else mp[1][m]=mp[2][1]=1;
  YES();
}

void DO13(){
  mp[1][1]=mp[n][m]=1;
  YES();
}

void SOLVE(){
  cin>>n>>m>>k;fl=0;
  if(n>m) swap(n,m),fl=1;
  for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) mp[i][j]=0;

  if(k>13||k==8||k==10||k==12) return NO();

  if(k==1) return DO1();
  if(n<=1&&m<=1) return NO();

  if(k==2) return DO2();
  if(k==5) return DO5();
  if(k==3) return DO3();

  if(n==1) return NO();

  if(k==4) return DO4();
  
  if(m<=2) return NO();
  
  if(k==6) return DO6();
  if(k==7) return DO7();
  if(k==9) return DO9();
  if(k==11) return DO11();

  if(n<=2) return NO();

  if(k==13) return DO13();
}


int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_;
  while(_--) SOLVE();
  return 0;
}
朴实无华的生成器
# include <bits/stdc++.h>
 
const int N=100010,INF=0x3f3f3f3f;
 
inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
 
int n,m;
int c[10][10];

inline int id(int x,int y){
	return (x-1)*m+y-1;
}
 
inline int v(int s,int x){
	return (s>>x)&1;
}
 
int d[10][10];
 
inline void solveL(int arr[][10]){
	for(int i=1;i<=n;++i){
		int cnt=0;
		for(int j=1;j<=m;++j) cnt+=arr[i][j];
		for(int j=1;j<=m;++j) arr[i][j]=(j<=cnt);
	}
	return;
}
inline void solveR(int arr[][10]){
	for(int i=1;i<=n;++i){
		int cnt=0;
		for(int j=1;j<=m;++j) cnt+=arr[i][j];
		for(int j=1;j<=m;++j) arr[i][m-j+1]=(j<=cnt);
	}
	return;
}
inline void solveU(int arr[][10]){
	for(int j=1;j<=m;++j){
		int cnt=0;
		for(int i=1;i<=n;++i) cnt+=arr[i][j];
		for(int i=1;i<=n;++i) arr[i][j]=(i<=cnt);
	}
	return;
}
inline void solveD(int arr[][10]){
	for(int j=1;j<=m;++j){
		int cnt=0;
		for(int i=1;i<=n;++i) cnt+=arr[i][j];
		for(int i=1;i<=n;++i) arr[n-i+1][j]=(i<=cnt);
	}
	return;
}
inline void rcopy(void){
	memcpy(d,c,sizeof(d));
	return;
}
inline int val(int arr[][10]){
	int s=0;
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) s=((s<<1)|arr[i][j]);
	return s;
	
}
inline void cover(int arr[][10],int s){
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) arr[i][j]=v(s,id(i,j));
	return;
}
 
int ret[20];
 
int pr[10][10];
 
int main(void){
	n=read(),m=read();
	
	int mask=(1<<(n*m))-1;
	
	memset(ret,-1,sizeof(ret));
	
	for(int s=1;s<=mask;++s){
		std::set <int> S;
		cover(c,s);
		rcopy(),S.insert(val(d));
		rcopy(),solveL(d),S.insert(val(d));
		rcopy(),solveR(d),S.insert(val(d));
		rcopy(),solveU(d),S.insert(val(d));
		rcopy(),solveD(d),S.insert(val(d));
		rcopy(),solveL(d),solveU(d),S.insert(val(d));
		rcopy(),solveL(d),solveD(d),S.insert(val(d));
		rcopy(),solveR(d),solveU(d),S.insert(val(d));
		rcopy(),solveR(d),solveD(d),S.insert(val(d));
		rcopy(),solveU(d),solveL(d),S.insert(val(d));
		rcopy(),solveU(d),solveR(d),S.insert(val(d));
		rcopy(),solveD(d),solveL(d),S.insert(val(d));
		rcopy(),solveD(d),solveR(d),S.insert(val(d));
		if(ret[S.size()]==-1) ret[S.size()]=s;
	}
	
	for(int i=0;i<=13;++i){
		if(ret[i]==-1) continue;
		
		printf("i = %d ret = %d\n",i,ret[i]);
		
		if(ret[i]!=-1){
			cover(pr,ret[i]);
			for(int x=1;x<=n;++x){
				for(int y=1;y<=m;++y) printf("%d",pr[x][y]);
				puts("");
			}	
		}	
	}
	return 0;
}

[CTS2024] 众生之门#

题解

打表发现答案不超过 3

进一步地,答案奇偶性固定,所以只需要检验答案是 0/1 还是 2/3

同时,我们还发现,取到最小值的解是不少的,解的大小可以近似看作在 [0,n] 内均匀分布。于是我们随机化 O(n) 次就很可能就能找到一组解。猜测除了 n 比较小的时候,以及树是菊花的情况,答案全部都 1。特判掉这些情况(如果 n 比较小,直接跑暴力;否则,怎么排列都是一样的)

当然,我们不能盲目蛮干。随机排列的复杂度肯定不能是 O(n) 的,我们可以先随机一个排列,然后随机交换相邻两项。为了减少常数,记得使用 O(1) LCA。

代码
/*
I know this sky loves you
いずれ全て
変わってしまったって
空は青いだろうよ
*/

# include <bits/stdc++.h>

const int N=50010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
int p[N];
int n,s,t;
std::vector <int> G[N];
int dfn[N],dep[N],tc,mx[N][21],fa[N];

void dfs(int x,int fa){
	dfn[x]=++tc,dep[x]=dep[fa]+1,mx[tc][0]=x,::fa[x]=fa;
	for(auto y:G[x]){
		if(y==fa) continue;
		dfs(y,x);
	}
	return;
}
inline int cmp(int x,int y){
	return dep[x]<dep[y]?x:y;
}
inline void init(void){
	for(int k=1;k<=17;++k)
		for(int i=1;i+(1<<k)-1<=n;++i) mx[i][k]=cmp(mx[i][k-1],mx[i+(1<<(k-1))][k-1]);
	return;
}

inline int lca(int u,int v){
	if(u==v) return u;
	u=dfn[u],v=dfn[v]; if(u>v) std::swap(u,v); ++u;
	int k=std::__lg(v-u+1);
	return fa[cmp(mx[u][k],mx[v-(1<<k)+1][k])];
}
inline int dis(int u,int v){
	return dep[u]+dep[v]-2*dep[lca(u,v)];
}

inline void print(void){
	for(int i=1;i<=n;++i) printf("%d ",p[i]); puts("");
	return;
}

std::mt19937 rng(114514);

inline bool flower(void){
	int dec=0; for(int i=1;i<=n;++i) if(G[i].size()>1) ++dec;
	return dec<=1;
}

int g[N];

inline void calc(void){
	if(flower()) return print();

	if(n<=10){
		int ans=INF;
		memcpy(g,p,(n+5)*4);
		do{
			int cur=0;
			for(int i=2;i<=n;++i) cur^=dis(p[i-1],p[i]);
			if(ans>cur) ans=cur;
			if(ans<=1) break;
		}while(std::next_permutation(p+2,p+n));
		memcpy(p,g,(n+5)*4);

		do{
			int cur=0;
			for(int i=2;i<=n;++i) cur^=dis(p[i-1],p[i]);
			if(ans==cur) return print();
		}while(std::next_permutation(p+2,p+n));
	}else{
		int T=std::min(30*n,400000);
		int ans=0;
		for(int i=2;i<=n;++i) ans^=dis(p[i-1],p[i]);
		while(T--){
			int x=rng()%(n-2)+2,y=rng()%(n-2)+2;
			if(x==y) continue;
			ans^=dis(p[x-1],p[x])^dis(p[x],p[x+1])^dis(p[y-1],p[y])^dis(p[y],p[y+1]);
			std::swap(p[x],p[y]);
			ans^=dis(p[x-1],p[x])^dis(p[x],p[x+1])^dis(p[y-1],p[y])^dis(p[y],p[y+1]);
			if(ans<=1) return print();
		}
		return print();
	}

	return;
}

inline void solve(void){
	n=read(),s=read(),t=read(),tc=0;
	p[1]=s,p[n]=t;
	for(int i=2,j=1;i<n;++i){ while(j==s||j==t) ++j; p[i]=j++;}
	for(int i=1;i<=n;++i) G[i].clear();
	for(int i=1;i<n;++i){
		int u=read(),v=read(); G[u].push_back(v),G[v].push_back(u);
	}
	dfs(1,0),init();
	calc();

	return;
}

int main(void){
	int T=read();
	while(T--) solve();
	return 0;
}

Part 3. 强化限制#

这部分构造的自由度通常有点太高,我们可以假装解还满足别的限制。

AGC035C Skolem XOR Tree#

题解

猜测树高只有 O(1) 层,因为树的形态越复杂我们构造的时候越不好控制。

首先还是发现,n=2k 的时候是没有解的。另外,异或是有性质的:任意四个连续数的异或和都是 0。这启发我们对这些数分成若干组,组间之间不干扰。

注意到把 1 当根的时候,对于每个 imod2=0,我们把 (i,i+1) 分成一组,然后构造一组 i+1i1i+n+1i+n 的链,最后把 n+1 挂到 3 的下面组成链 n+1321 即可。

最后的问题是 nmod2=0 的情况。我们判掉了 n=2k 的情况,遂猜测 n 的连边可能跟 n 的 popcount 至少是 2 有点关系。事实也是如此,令 vn 的 lowbit,则 v 也是偶数。构造链 nn+v+11nv2n 即可。上述链不会破坏之前构造出的结构,因为 1 此时和编号小于 n 的偶数点、编号大于 n+1 小于 2n 的奇数点都有连边。

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
int n;

int main(void){
	if(__builtin_popcount(n=read())==1) puts("No"),exit(0);
	puts("Yes");
	for(int i=2;i+1<=n;i+=2){
		printf("%d %d\n%d %d\n%d %d\n%d %d\n",1,i,1,i+n+1,i,i+1,i+n,i+n+1);
	}
	printf("%d %d\n",3,n+1);
	if(n%2==0){
		int v=n&-n; printf("%d %d\n%d %d\n",n,n+v+1,2*n,n-v);
	}

	return 0;
}

Codeforces 804E The same permutation#

题解

结合上 Part 1 的技巧,首先考虑:什么时候无解?注意到交换要改变逆序对奇偶性,那么 n(n1)/2 如果不是偶数就无解了。因此 nmod41

这个时候已经只剩下 nmod4=0,nmod4=1 的两种情况了。先来考虑 nmod4=0 的情况。

一种合理的尝试是将元素按 4 个一组分组。因为具体是什么元素不重要,于是我们可以假装分组是 (1,2,3,4),(5,6,7,8),, 这样。

搜索发现存在一系列操作刚好消耗完组内的 6 个操作;同时,存在一系列操作能够消耗完两组间的所有 16 个操作(这个其实也是可以搜出来的,大致思路是:我们感知一下,可以把 16 个操作分成 4 组,使得每组内每个元素只参与恰好 1 次交换;这样搜索复杂度就大大减少了)。因此按照任意顺序执行组内 / 组外操作即可。

有了 nmod4=0 的情况,对于 nmod4=1 的情况,就不太困难了。我们只需要在适当的时候插入有 n 参与的操作就行了。具体地,对于一个组内操作 (x,y),其中 xmod2=1,y=x+1,我们可以等价替换为 (x,n),(x,y),(y,n)

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,v[N];
std::vector <std::array <int,2> > ans;


inline void oper(std::array <int,2> s,bool flag=false){
	if(flag&&(n&1)&&(s[0]%2)&&(s[0]+1==s[1])){
		ans.push_back({s[0],n}),ans.push_back({s[0],s[1]}),ans.push_back({s[1],n});
	}else ans.push_back(s);
	std::swap(v[s[0]],v[s[1]]);
	return;
}

inline void inblo(int l){
	--l;
	oper({l+1,l+2},true);
	oper({l+1,l+3},true);
	oper({l+2,l+4},true);
	oper({l+1,l+4},true);
	oper({l+2,l+3},true);
	oper({l+3,l+4},true);
	return;
}
inline void betblo(int l,int r){
	--l,--r; // (a b c d) (b c d a) (d a b c) (c d a b)
	oper({l+1,r+1});
	oper({l+2,r+2});
	oper({l+3,r+3});
	oper({l+4,r+4});

	oper({l+1,r+2});
	oper({l+2,r+3});
	oper({l+3,r+4});
	oper({l+4,r+1});
	
	oper({l+1,r+4});
	oper({l+2,r+1});
	oper({l+3,r+2});
	oper({l+4,r+3});

	oper({l+1,r+3});
	oper({l+2,r+4});
	oper({l+3,r+1});
	oper({l+4,r+2});
	return;
}

int main(void){
	n=read(),std::iota(v+1,v+1+n,1);
	if(n==1) return puts("YES"),0;
	if(n%4>1) return puts("NO"),0;
	
	for(int i=1;i<=n/4;++i) inblo((i-1)*4+1);
	for(int i=1;i<=n/4;++i){
		for(int j=i+1;j<=n/4;++j) betblo((i-1)*4+1,(j-1)*4+1);
	}
	
	for(int i=1;i<=n;++i) assert(v[i]==i);
	
	puts("YES");
	for(auto w:ans) printf("%d %d\n",w[0],w[1]);

	return 0;
}

Part 4. 归纳构造#

nn+knnknkn

UOJ496 新年的新航线#

题解

(From KING_OF_TURTLE)

对于 n>4 的情形:

考虑多边形最外侧的三角形 (u,v,w),设 w 是外侧点,可以发现与 w 相邻的两条边中必定删除恰好一条。

先删除这两条边,然后给三角形的内侧边 (u,v) 打上一个标记 w,表示之后 w 要向 (u,v) 两个点中的恰好一个连边。

接下来,我们每次考虑删除的时候,要考虑边上的标记。对于最外侧的三角形 (u,v,w),设 w 为外侧的点,分情况讨论:

  • (u,w)(v,w) 均无标记:将 w 打到 (u,v) 上;
  • (u,w)(v,w) 中恰有一个有标记:设 (u,w) 有标记,把标记点和 w 都连到 u 上面;(因为 u 成为外侧点的时候还要连至少一条边所以是 OK 的)
  • (u,w)(v,w) 均有标记:将标记点都连到 w 上,再把 w 打到 (u,v) 上。

n=4 简单讨论或直接搜索均可。

代码
# include <bits/stdc++.h>

const int N=500010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,du[N];
std::vector <int> G[N];
bool del[N];
std::map <std::pair <int,int>,int> tag;

inline int find(int u,int v){
	if(u>v) std::swap(u,v);
	return tag[std::make_pair(u,v)];
}

inline void join(int u,int v,int w){
	if(u>v) std::swap(u,v);
	tag[std::make_pair(u,v)]=w;
	return;
}

std::vector <std::pair <int,int> > ans;

inline void res(int u,int v){
	ans.push_back(std::make_pair(u,v));
	return;
}

int main(void){
	n=read();
	
	if(n==3) puts("-1"),exit(0);
	
	for(int i=1;i<=n-3;++i){
		int u=read(),v=read();
		G[u].push_back(v),G[v].push_back(u),++du[u],++du[v];
	}
	
	for(int i=1;i<=n;++i){
		int u=i,v=i%n+1;
		G[u].push_back(v),G[v].push_back(u),++du[u],++du[v];
	}
	
	std::queue <int> q;
	
	for(int i=1;i<=n;++i) if(du[i]==2) q.push(i);
	
	for(int T=1;T<=n-4;++T){
		int i=q.front(),u=0,v=0;
		q.pop(),del[i]=true;
		
		for(auto j:G[i]){
			--du[j];
			if(!del[j]){
				if(!u) u=j;
				else v=j;
				if(du[j]==2) q.push(j);
			}
		}
		
		int tu=find(i,u),tv=find(i,v);
		
		if(!tu&&!tv){
			join(u,v,i);
		}else if(tu&&tv){
			res(tu,i),res(tv,i);
			join(u,v,i);
		}else if(tu){
			res(tu,u),res(i,u);
		}else res(tv,v),res(i,v);
	}
	
	std::vector <int> d2,d3;
	
	for(int i=1;i<=n;++i) if(!del[i]){
		if(du[i]==2) d2.push_back(i);
		else d3.push_back(i);
	}
	
	bool l[2]={false,false};
	
	if(find(d2[0],d3[0])) res(find(d2[0],d3[0]),d3[0]),l[0]=true;
	if(find(d2[1],d3[0])) res(find(d2[1],d3[0]),d3[0]),l[0]=true;

	if(find(d2[0],d3[1])) res(find(d2[0],d3[1]),d3[1]),l[1]=true;
	if(find(d2[1],d3[1])) res(find(d2[1],d3[1]),d3[1]),l[1]=true;

	if(!l[0]&&!l[1]) res(d3[0],d3[1]),res(d3[0],d2[0]),res(d3[0],d2[1]);
	else if(l[0]&&l[1]) res(d3[0],d3[1]),res(d2[0],d3[0]),res(d2[1],d3[1]);
	else{
		if(l[1]) std::swap(d3[0],d3[1]); 
		res(d3[0],d3[1]),res(d3[0],d2[0]),res(d3[0],d2[1]);
	}

	for(auto v:ans) printf("%d %d\n",v.first,v.second);

	return 0;
}

QOJ7756 Omniscia Spares None#

题解

「换斗移星,谋事在人」

对于 n4 的情况是平凡的。对于 n=5,6 的情况目测无解。对于 n=7,9 的情况均无解,但 n=8,n=10 的情况均有解。

手玩应该是可以玩出来的,但是草稿纸弄掉了,火大。这里贴一个别人的构造吧:

在构造的过程中,我们尽可能把度数小于 6 的点往外放,力求增量构造的时候把之前度数小于 6 的点变为度数不小于 6 的点。尝试之后,发现可以放出来两个度数为 3 的点。因此,一种增量构造方法如下:

此时最上方和内部的点度数为 3,左右两角的点度数为 6

image-20241230164849400

使用这种方法添加了 4 个点并保持了原来的结构。

因为我们有 n=8,10 的构造,可以按照这种方法构造出其余偶数的解。大胆猜测 n 是偶数都有解,n 是奇数则无解。

代码略。

Codeforces 715D Create a Maze#

题解

注意到 n,m,k 的级别仅为 O(logT),这启发我们要若干个乘 kb 的操作。

2×2 实现乘 20/1 是简单的,如下图:

image

但是这道题限制相当紧,无法通过。我们发现我们对左侧的利用不够:左侧和上侧都可以利用,但是因为进制取得太小,导致左侧只能被完全封上。

尝试取 3×3 进行基本构造。

image

这样,组合三个 ? 处的开 / 关,可以实现 3×3 完成乘 6k(0k<6),足以通过本题。

取其它进制或者进行合理的随机化也可以通过,可以自行查阅其它题解。

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
typedef long long ll;
ll T;

std::vector <int> d;
int n;
std::vector <std::array <int,4> > vec;

inline void init(int x){
	n+=2;
	vec.push_back({n-4,n,n-3,n});
	vec.push_back({n-6,n-1,n-5,n-1});
	vec.push_back({n,n-4,n,n-3});
	vec.push_back({n-1,n-6,n-1,n-5});
	vec.push_back({n,n-1,n+1,n-1});
	vec.push_back({n-2,n,n-2,n+1});
	vec.push_back({n-1,n,n-1,n+1});
	vec.push_back({n,n-2,n+1,n-2});
	vec.push_back({n-3,n-1,n-2,n-1});
	if(x<3) vec.push_back({n-1,n-3,n-1,n-2}); else x-=3;
	if(x<2) vec.push_back({n,n-3,n,n-2}); else --x;
	if(x<1) vec.push_back({n-3,n,n-2,n});
	return;
}

inline bool chk(std::array <int,4> v){
	if(v[0]<1||v[1]<1||v[2]>n||v[3]>n) return false;
	return true;
}

int main(void){	
	scanf("%lld",&T);
	while(T) d.push_back(T%6),T/=6;
	std::reverse(d.begin(),d.end());
	n=2; vec.push_back({2,1,2,2}),vec.push_back({1,2,2,2});
	for(auto x:d) init(x);
	int sum=0;
	for(auto v:vec) sum+=chk(v);
	printf("%d %d\n",n,n);
	printf("%d\n",sum);
	for(auto v:vec) if(chk(v)){
		for(auto w:v) printf("%d ",w); puts("");
	}

	return 0;
}

Part 5. 图论构造杂项#

都看到这里了,来看点开心的东西吧。

图上二选一构造#

通常是用 DFS 树这个结构。被 EA 锐评:出这个简直有病!

来源:【学习笔记】一类图上二选一构造问题 - duyiblue - 博客园。原文总结得相当到位,这里就不补充了。

图上转圈构造#

题 1

把⼀个 n 个点的完全图的所有边排成一个环,使得任意连续的 n1 条边都构成一棵树。

image

image

题 2.1

把一个 n 个点的完全图分成 n1 组匹配,n 为偶数。

image

题 2.2

把一个 n 个点的完全图分成 n/2 组哈密顿路,n 为偶数。

image

题 2.3

把一个 n 个点的完全图分成 n12 组哈密顿回路,n 为奇数。

image

Part 6. 实战#

Codeforces 341E Candies Game#

题解

考虑有三堆大小分别为 x,y,z 的糖果时怎么做。

y=kx+b。对 k 二进制拆分,从低到高考虑 k 的每一位,设当前位位权为 2l

  • k2l 位为 1,则执行操作 (x,y),此时 k2l 位被夺去,x 翻倍;
  • k2l 位为 0,则执行操作 (x,z)(不难验证仍有 xz),此时 x 翻倍。

那么操作后 y 变为 b。排序后,新的 x,y,z 满足 x<12x

O(log2V) 次减少一堆,于是总次数大概是 O(nlog2V) 的,可以通过。

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n;
int a[N];
std::vector <int> idx;

inline int val(void){
	idx.clear();
	for(int i=1;i<=n;++i) if(a[i]) idx.push_back(i);
	return (int)idx.size();
}

std::vector <std::array <int,2> > ans;

inline void move(int x,int y){
	assert(a[x]<=a[y]); ans.push_back({x,y}); a[y]-=a[x],a[x]*=2;
	return;
}

inline void solve(int x,int y,int z){
	if(a[x]>a[y]) std::swap(x,y); if(a[y]>a[z]) std::swap(y,z); if(a[x]>a[y]) std::swap(x,y);
	int k=a[y]/a[x];
	for(int i=0;i<=std::__lg(k);++i){
		if((k>>i)&1) move(x,y); else move(x,z);
	}
	return;
}

int main(void){
	n=read();
	for(int i=1;i<=n;++i) a[i]=read();
	if(val()<=1) puts("-1"),exit(0);
	while(val()>=3) solve(idx[0],idx[1],idx[2]);
	printf("%d\n",(int)ans.size());
	for(auto v:ans) printf("%d %d\n",v[0],v[1]);

	return 0;
}

Codeforces 538G Berserk Robot#

题解

先判断奇偶性是否合法。

考虑进行坐标转换:(x,y)(x+y+t2,yx+t2)。本质上是,先旋转坐标系使得两维度独立,再加上 t 使得坐标变化量非负。

这样,两维是独立的。接下来以 x 这一维举例。我们本质上想求一个周期内的位移 v。为此,对于第 i 条信息 (ti,xi),我们记录 ki=ti/lwi=timodl。将信息按照 w 排序之后(因为有边界情况,我们加入信息 (k=0,w=0)(k=1,w=l) ),考虑相邻两项。

注意到一个周期的位移是 v,周期内 wi1wi 这段时间的位移可以是 [0,Δw],那么我们有限制:

  • ΔxwΔk×vΔx

解不等式即可。

如果有解我们可以根据 vki,xi 还原出第 wi 秒应该在哪个位置。那么有构造:如果当前还没走到那个位置,就增加坐标,否则在那个位置旁边横跳即可。如果奇偶性是正确的,这个办法总是可行。

代码
# include <bits/stdc++.h>

const int N=200010,INF=0x3f3f3f3f;

typedef long long ll;

inline ll read(void){
	ll res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,l;
struct Node{
	ll t,x,y,k,w;
	bool operator < (const Node &rhs) const{
		return (w!=rhs.w)?(w<rhs.w):(k<rhs.k);
	}
}p[N];

template <typename T> inline void cmax(T &a,T b){
	a=std::max(a,b);
}
template <typename T> inline void cmin(T &a,T b){
	a=std::min(a,b);
}

int main(void){
	n=read(),l=read();
	for(int i=1;i<=n;++i){
		ll t=read(),x=read(),y=read(); // (x+y+t)/2, (y-x+t)/2
		if((t&1)!=((x+y)&1)) puts("NO"),exit(0);
		p[i].t=t,p[i].x=(x+y+t)/2,p[i].y=(y-x+t)/2;
		p[i].k=t/l,p[i].w=t%l;
	}

	p[++n].k=-1,p[n].w=l;
	p[++n].k=0,p[n].w=0;
	std::sort(p+1,p+1+n);

	ll xmin=0,xmax=l,ymin=0,ymax=l;

	for(int i=2;i<=n;++i){
		ll k=p[i].k-p[i-1].k;
		ll w=p[i].w-p[i-1].w;
		ll dx=p[i].x-p[i-1].x,dy=p[i].y-p[i-1].y;
		
		if(k==0){
			if(!(dx-w<=0&&0<=dx)) puts("NO"),exit(0);
			if(!(dy-w<=0&&0<=dy)) puts("NO"),exit(0);
		}else if(k>0){
			ll l=(ll)ceill(1.0L*(dx-w)/k),r=(ll)floorl(1.0L*dx/k);
			cmax(xmin,l),cmin(xmax,r);
			l=ceill(1.0L*(dy-w)/k),r=(ll)floorl(1.0L*dy/k);
			cmax(ymin,l),cmin(ymax,r);
		}else{
			k*=-1;
			ll l=(ll)ceill(1.0L*(-dx)/k),r=(ll)floorl(1.0L*(-dx+w)/k);
			cmax(xmin,l),cmin(xmax,r);
			l=(ll)ceill(1.0L*(-dy)/k),r=(ll)floorl(1.0L*(-dy+w)/k);
			cmax(ymin,l),cmin(ymax,r);
		}
	}
	
	if(xmin>xmax||ymin>ymax) puts("NO"),exit(0);

	std::vector <int> xm,ym;

	for(int i=2;i<=n;++i){
		if(p[i].w==p[i-1].w) continue;
		ll pre=p[i-1].x-p[i-1].k*xmin,cur=p[i].x-p[i].k*xmin;
		int td=p[i].w-p[i-1].w;
		for(int j=1;j<=td;++j) if(pre<cur) xm.push_back(1),++pre; else xm.push_back(0);
		pre=p[i-1].y-p[i-1].k*ymin,cur=p[i].y-p[i].k*ymin;
		for(int j=1;j<=td;++j) if(pre<cur) ym.push_back(1),++pre; else ym.push_back(0);
	}

	for(int i=0;i<l;++i) putchar("DLRU"[xm[i]<<1|ym[i]]);

	return 0;
}

Codeforces 1276E Four Stones#

题解

仍然是要先考虑判断无解的。将石子排序之后差分,不难观察到差分的 gcd 是不变的,于是,如果两个状态的差分 gcd 不同就无解。同时奇偶性也是不会变的,于是还要检查奇偶性。

否则石子的坐标总能够直接表示为 x=xg+r。接下来讨论所有石子的坐标均已经变化为 x 的情形,此时差分 gcd1

注意到操作可逆。我们想办法从起始状态和终止状态出发,得到两个比较简单的状态,如果能够在这两个比较简单的状态之间移动,那么就解决了。

这里一个简单的状态是:位置的极差 1。更进一步地,我们会移动到 [p,p+1] 处,其中 p 是一个偶数。考虑如何达到这个状态。

设石子的位置为 x1x2x3x4Δ=x4x1。如果 [x1+1/4Δ,x41/4Δ] 中间有某个 x2 或者 x3,那么将 x1,x4 中离这个 x2/x3 更近的那一个翻到另一侧去,可以让 Δ 变为原来的至多 3/4

否则,我们记 d2=min(x2x1,x4x2),d3=min(x3x1,x4x3)。此时都有 d2,d3<1/4Δ。考虑快速增大 d2,d3 使得其中某一个达到至少 1/4Δ,就可以执行上面的操作了。

考虑如下操作:将 a 沿着 b,c 翻折,a 会先变为 2ba,再变为 2c(2ba)=a+2(cb)。那么 d2,d3 中较小的那个总能够通过这种方式来增加较大的那个的两倍。不难发现 O(logΔ) 次后就会变得符合条件。

设起始状态和终止状态分别移动到了 [ps,ps+1][pt,pt+1]。我们希望将 [ps,ps+1] 移动到 [pt,pt+1] 处。不妨设 ps<pt,有一个简单的每次平移 2 的方法:执行两次「将 x1,x2,x3 对着 x4 翻折」的操作。此时操作次数太多,无法通过。

注意到每次移动的距离等于二倍极差。同时,执行「将 x3 对着 x1 翻折,x2 对着 x4 翻折」的操作会使得极差变为原来的至少两倍。于是,我们可以采取这样的方法:先按照这种方法增大极差,直到极差即将超过 ptps,然后执行平移。平移后,如果当前极差已经超过 ptps,则撤销一次这种操作,减少极差。

这样总操作次数是 O(log2V) 级别的,大部分时候不太满,可以通过此题。

代码
# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

typedef long long ll;

typedef std::array <ll,2> po;

std::vector <po> A,B;
std::vector <po> ra,rb;

inline void sym(std::vector <po> &s,std::vector <po> &ret,int i,int j,bool real=false){
	if(real){
		for(int x=0;x<=3;++x) if(s[x][1]==i){i=x; break;}
		for(int x=0;x<=3;++x) if(s[x][1]==j){j=x; break;}
	}
	
	s[i][0]=2*s[j][0]-s[i][0];
	ret.push_back({s[i][1],s[j][1]});
}
inline void chk(std::vector <po> &s){std::sort(s.begin(),s.end());}

inline void gather(std::vector <po> &s,std::vector <po> &ret){
	for(;;){
		chk(s);
		ll d=s[3][0]-s[0][0]; assert(d);
		if(d==1) break;
		
		bool flag=false;
		
		for(int j=1;j<=2;++j){
			if(s[j][0]>=s[0][0]+d/4.0&&s[j][0]<=s[3][0]-d/4.0){
				if(s[j][0]-s[0][0]<=s[3][0]-s[j][0]) sym(s,ret,0,j);
				else sym(s,ret,3,j);
				flag=true; break;
			}
		}
		if(flag) continue;
		
		ll dl=std::min(s[1][0]-s[0][0],s[3][0]-s[1][0]);
		ll dr=std::min(s[2][0]-s[0][0],s[3][0]-s[2][0]);
		if(dl<dr){// [0 1]     [2     3]
			if(s[1][0]-s[0][0]==dl){
				if(s[3][0]-s[2][0]==dr) sym(s,ret,1,2),sym(s,ret,1,3); // [2,3]
				else sym(s,ret,1,0),sym(s,ret,1,2); // [0,2]
			}else{
				if(s[3][0]-s[2][0]==dr) sym(s,ret,1,3),sym(s,ret,1,2);
				else sym(s,ret,1,2),sym(s,ret,1,0);
			}
		}else{
			if(s[3][0]-s[2][0]==dr){
				if(s[1][0]-s[0][0]==dl) sym(s,ret,2,1),sym(s,ret,2,0); // [0,1]
				else sym(s,ret,2,3),sym(s,ret,2,1); // [1,3]
			}else{
				if(s[1][0]-s[0][0]==dl) sym(s,ret,2,0),sym(s,ret,2,1);
				else sym(s,ret,2,1),sym(s,ret,2,3);
			}
		}
	}
	chk(s);
	if(s[0][0]&1) for(int j=0;j<=3;++j) if(s[j][0]!=s[3][0]) sym(s,ret,j,3);
	chk(s);
	return;
}

inline ll myabs(ll x){return x>0?x:-x;}

inline void slide(void){
	ll s=A[0][0],t=B[0][0];
	if(s==t) return;
	
	std::vector <po> ex;
	
	ll l=myabs(t-s)/2; bool fl=false;
	for(;;){
		chk(A); ll d=A[3][0]-A[0][0];
		if(l==0&&d==1) break;		
		while(!fl&&d<l){
			chk(A); sym(A,ex,2,0),sym(A,ex,1,3); chk(A);
			ll _d=A[3][0]-A[0][0];
			d=_d;
			ra.push_back(ex[ex.size()-2]),ra.push_back(ex[ex.size()-1]);
		}
		if(fl) fl=false;	
		if(d>l){
			fl=true;
			auto u=ex[ex.size()-2],v=ex[ex.size()-1];
			sym(A,ra,v[0],v[1],true),sym(A,ra,u[0],u[1],true);
			ex.pop_back(),ex.pop_back();
			continue;
		}
		
		l-=d;
		for(int _=0;_<=1;++_){
			chk(A);
			if(s<t) for(int i=0;i<=2;++i) sym(A,ra,i,3);
			else for(int i=1;i<=3;++i) sym(A,ra,i,0);
		}
	}
	return;
}


int main(void){	
	A.resize(4),B.resize(4); 
	for(int i=0;i<=3;++i) A[i][0]=read(),A[i][1]=i;
	for(int i=0;i<=3;++i) B[i][0]=read(),B[i][1]=i;

	auto C=A;
	std::sort(A.begin(),A.end()); std::sort(B.begin(),B.end());
	
	ll ga=0,gb=0,d=0;
	for(int i=1;i<=3;++i) ga=std::__gcd(A[i][0]-A[0][0],ga),gb=std::__gcd(B[i][0]-B[0][0],gb);
	if(ga!=gb||(ga!=0&&(A[0][0]%ga!=B[0][0]%gb))) puts("-1"),exit(0);
	if(ga==0){
		if(A[0][0]==B[0][0]) puts("0"),exit(0);
		else puts("-1"),exit(0);
	}
	d=A[0][0]%ga;
	for(int i=0,c[2]={0,0};i<=3;++i){
		A[i][0]=(A[i][0]-d)/ga,B[i][0]=(B[i][0]-d)/gb;
		++c[A[i][0]&1],--c[B[i][0]&1];
		if(i==3&&(c[0]||c[1])) puts("-1"),exit(0);
	}
	
	gather(A,ra),gather(B,rb);
		
	slide();
	chk(A);
	
	for(int i=0;i<=3;++i) assert(A[i][0]==B[i][0]);
	std::vector <int> bid(4);
	for(int i=0;i<=3;++i) bid[B[i][1]]=A[i][1];
	
	while(rb.size()) ra.push_back({bid[rb.back()[0]],bid[rb.back()[1]]}),rb.pop_back();
	
	std::vector <po> real_ans;
	for(auto v:ra){
		if(C[v[0]][0]!=C[v[1]][0]) real_ans.push_back({C[v[0]][0],C[v[1]][0]});
		C[v[0]][0]=2*C[v[1]][0]-C[v[0]][0];
	}
	printf("%d\n",(int)real_ans.size());
	for(auto v:real_ans) printf("%lld %lld\n",v[0],v[1]);

	return 0;
}

Codeforces 1053E Euler tour#

题解

考虑对于一个给定的序列 a1,a2,,a2n1,判定是否合法:

  • a1=a2n1
  • 对于任意 al=arrl 应该是偶数;
  • 对于任意 al=ar(l,r) 应该恰好出现 (rl)/2 种数;
  • 对于每种数,第一次出现位置和最后一次出现位置构成的区间,要么包含要么不交。

现在可以开始做了。设我们在处理 [l,r] 这个区间,我们想要 [l,r] 刚好是一棵完整的子树。那么 al=ar 必须成立(或者有 al=ar=0),且 rl 应该是偶数。接下来处理区间内部,我们遍历 (l,r) 每一个非零位置 ax,并且找到下一个和 ax 相等的位置 ay。如果 y 在区间外部那么已经非法,否则 (x,y) 也构成一棵完整的子树,我们就把 [x,y] 缩成一个点 ax,然后找下一个可能存在的 y。重复该过程,此时 (l,r) 内不会有相同数。

如果此时 (l,r) 中出现的种类数大于 (rl)/2,已经无解;否则在区间开头的若干个 0 处填上新出现的数。接下来采用如下方式填上剩余的 0

  • 首先,若连续三个位置形如 [0,a,b],则变为 [b,a,b] 然后缩成单点 b;对于 [a,b,0] 同理。
  • 因为此时 (l,r) 中已经填上了 (rl)/2 个数,经过上面的步骤之后,必然不会有相邻的两个 0,此时给区间填上子树的根一定是合法的。

使用链表维护做到线性。

代码
# include <bits/stdc++.h>

const int N=1000010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int n,m;
int a[N];
int cur;
bool vis[N];

inline void wrong(void){
	printf("no"),exit(0);
}

inline int newval(void){
	while(cur<=n&&vis[cur]) ++cur;
	if(cur>n) wrong();
	return cur;
}

int pre[N],nex[N];
int mat[N],pos[N];

inline void del(int l,int r){
	l=pre[l],r=nex[r];
	pre[r]=l,nex[l]=r;
	return;
}
inline void assign(int l,int r,int &x){
	while(x>l&&nex[nex[x]]<=r&&!a[x]&&a[nex[x]]&&a[nex[nex[x]]]){
		a[x]=a[nex[nex[x]]],del(nex[x],nex[nex[x]]),x=pre[pre[x]];
	}
	while(x>l&&nex[nex[x]]<=r&&a[x]&&a[nex[x]]&&!a[nex[nex[x]]]){
		a[nex[nex[x]]]=a[x],
		del(nex[x],nex[nex[x]]),x=pre[pre[x]];
		
	}
	return;
}

void solve(int l,int r){
	if((r-l+1)%2==0) wrong();
	for(int i=l;i<=r;i=nex[i]){
		while(mat[i]){
			if(mat[i]>r) wrong();
			solve(nex[i],pre[mat[i]]),del(nex[i],mat[i]),mat[i]=mat[mat[i]];
		}
	}
	int cur=0,tot=0,rt=a[pre[l]];
	for(int i=l;i<=r;i=nex[i]) cur+=(a[i]!=0),++tot;
	tot=tot/2+1;
	if(cur>tot) wrong();
	for(int i=l;i<=r;i=nex[i]) if(cur<tot&&!a[i]) a[i]=newval(),vis[a[i]]=true,++cur;
	for(int i=l;i<=r;i=nex[i]) assign(pre[l],r,i);	
	for(int i=l;i<=r;i=nex[i]) if(!a[i]) a[i]=rt;
	return;
}

int main(void){
	n=read(),m=2*n-1;
	for(int i=1;i<=m;++i) a[i]=read(),vis[a[i]]=true;
	if(n==1) printf("yes\n%d",1),exit(0);
	
	for(int i=2;i<=m;++i) if(a[i]&&a[i-1]&&a[i]==a[i-1]) wrong();
	if(a[1]&&a[m]&&a[1]!=a[m]) wrong();
	a[1]=a[m]=(a[1]?a[1]:a[m]);
	
	for(int i=1;i<=m;++i) pre[i]=i-1,nex[i]=i+1;
	pre[0]=0,nex[0]=1,nex[m+1]=m+1,pre[m+1]=m;
	
	for(int i=m;i;--i){
		if(!a[i]) continue;
		mat[i]=pos[a[i]],pos[a[i]]=i;
	}
	
	solve(1,m);
	printf("yes\n");
	for(int i=1;i<=m;++i) printf("%d ",a[i]);

	return 0;
}
 

如果确实是构造爱好者的话!#

请看 [AGC043E] Topology,zhouershan 认证好题。

作者:Meatherm

出处:https://www.cnblogs.com/Meatherm/p/18623711

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Meatherm  阅读(79)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示