Constructive Algorithms!
Constructive Algorithms!#
前言#
构造题,顾名思义,就是有 constructive algorithms 标签的题目。它们往往给出某些若干限制,并要求选手构造出符合这些限制的对象,例如序列、图和树等。当然,对于某些交互题,构造具体的询问策略也可以视作一类构造题。甚至,一道带输出方案的数据结构题也可能是构造题。这类题目往往不好用某一类特定算法来解决,只能对具体问题具体分析。
然而,尽管题与题之间最终算法 / 构造出的方案差距很大,这类题目还是有一些基本的技法和分析技巧。本篇挑选了一些有代表性的题目,旨在展示面对在各种风格迥异的题目的时候,一些可能有效的分析方法,以及一些可能可参考的,逐步得到可行解的思考路径。
可能有用的方法#
如果直接在方法处插入对应例题就有点奇怪:这样提示就太明显了。
所以这一节是一些不结合题目的纸上谈兵。请仔细体会。
方法零:简单尝试#
很多选手的注意力相当集中。他们往往可以直接观察到一组解,然后通过;或是尝试一些简单策略,然后通过。如果一个题短时间内通过了非常多人,那么就可以试试枚举简单策略。
方法一:精细构造#
对于一些题目,构造的限制可能比较严格。但是,我们可以相对容易地得到离限制较为接近的一组解。这个时候,重新审视构造这组解的过程,我们可能发现某一些部分可以更优,从而,对这一组解微调就可以得到满足限制的解。
例如:
- 递归到小范围的时候,可以直接使用搜索得出的最优解,而不是人工构造的可行解;
- 交互题的询问进行到某个阶段的时候,可以复用一部分之前得出的信息;
- 构造进行到某个阶段的时候,当前局面的性质已经比初始时更好,从而可以换用常数更小的操作方法继续构造。
方法二:打表 (小范围搜索)#
对于限制越少的题目,打表的用武之地就越大。一般来说,题目的输入量越少(例如只有一个 ),构造的自由度就越大。这个时候,解的组数会很多,最优解 / 最小字典序解通常也会具有某种规律。观察出这些规律,就有较大概率通过题目。
例如:
- 直接搜索得出 较小时的一组解,然后推广到一般情况;
- 搜索得出有解的情况,只针对有解情况进行人工构造,避免虚空做题;
- 搜索一些特殊局面下的解,试图获得启发,然后推广到一般情况。
当然,打表并非要依赖输入量少这一性质。部分题目中,尽管输入规模可能达到 级别,但是稍加分析,可以发现,大部分输入都不如特定的某组极限数据强。这个时候,如果能打表得出极限数据的规律,那么稍加微调自然可以解决更弱的输入。
然而,打表也不是万能的。有的时候简单的可行解不是最优解 / 最小字典序解,而是混在解集中,难以辨认。另外,对于部分题目,随着 的增长,搜索范围可能会迅速增大(譬如:搜索的时间复杂度是 或者 ),从而使得搜索运行时间无法接受。
方法三:从解的形式出发#
这里包含两类方法:
-
考虑有解的条件。
对于可能会区分有 / 无解的题目,如果暂且不明确哪些情况会无解,可以先思考一些无解的充分条件。这些充分条件可以排除掉一些情况,使得关于剩下的情况的构造更为简明。如果能证明剩下的情况都可以被构造出来那么就解决了。
例如: 是奇 / 偶数的时候无解,由此延伸出各种处理方法:将 配对构造,或者 配对构造,等等。
-
考虑解具体是什么。
一类最优化问题通常是,让你构造出一组最优解,但是这个最优值具体是多少还不明了。可以考虑先找出这个最优值,再根据这个最优值的形式进行具体构造。
方法四:强化题目限制#
和方法一中那些需要精细构造的题目不同,很多时候题目的自由度太高,解集太大,反而导致无从下手。我们可以自行强化题目限制,缩小解集大小,从而可以结合上人工构造 / 打表等一系列操作。
涉及到不同对象的时候,强化的方法也有区别。这里没有什么好的概括。
方法五:归纳构造#
没什么好的办法的时候可以试试归纳。这样只需要考虑如何从 或 ,比直接构造整个解要好一些。
当然有的时候肯定不能满足于 ,还要结合上 的操作(通常出现在若干次 次构造出 这样的题目里面)。
例如:
- 给当前局面设计一个势能,然后找到一组操作,使得在操作之后这个局面的势能减少。只要对于每个局面都设计这样一组操作就可以了。
- 人工构造 / 搜索出小范围的解,然后找到若干组支持 ()的操作。组合这些操作得到答案。
方法六:构造基本操作#
有的时候,题目中给出的可用的操作非常奇怪,不符合我们的认知习惯。这个时候可以尝试组合题中的可用操作,得到符合我们认知习惯的基本操作,然后在擅长的领域内解决问题。
例如:组合操作得出循环移位 / 交换 / 给某个位置减少 ,然后利用组合后的操作解决问题。
方法七:随机化 / 乱搞#
实在做不出来的时候也可以试试这个,构造的自由度越高,效果越好,有的时候可能有奇效。
Part 0. 热身题#
这一部分题目不需要很多前面提到的方法。在一定时间的尝试之后我们便可以得到一组看上去正确的解,对这组解微调就可以通过。当然,微调也许是困难的。
AGC052A Long Common Subsequence#
题解
我们发现构造出长度等于 的 LCS 非常简单,可以猜测长度为 的 LCS 也不太困难。
一种合理的猜测是,感觉构造和这个 有点关系,因为 ,说不定只给出 个串就是干这个用的呢?
但事实上没有关系,因为我们是有通解的。简单枚举之后发现:输出 个 ,然后 个 ,然后一个 即可通过。这也没什么办法,构造总是需要多多尝试。
证明:若 中最后一个位置是 ,那么在 的前 个字符中取出 个 ,中间 个字符中取出 个 ,最后一个字符固定为 ;否则,设 中极长全 后缀长度为 ,那么在 的前 个字符中取出 个 和 个 ;而我们只需要在后 个字符中取出前 个 ,因为这是极长连续段,所以取完第 个 之后后面必然还剩下至少一个 。
Codeforces 1227G Not Same#
题解
题目可以转化为:在 的网格内填 ,使得第 列的和恰好为 ,且每一行互不相同。
考虑 全部为 的时候我们有简单策略,也就是:第 列从第 行开始填 个 ,填出去了就从第 行开始继续填。
简单尝试发现:当 降序排序的时候,采用类似的方法,第 列从第 行开始填 个 。
证明留作习题。
习题解答
反证法。不妨设第 行和第 行 完全相同。
如果 ,考虑 的值,它必须为 ,因为 。根据假设, 也必须为 ,那么就导出 。因为 ,从而 和 必须全部为 ,这样就得出了 。反复运用会得到 ,而按照填数规则和假设, 和 必须全部为 ,这导出 ,从而矛盾。
如果 ,我们容易得出 。运用类似的分析,同样容易推出矛盾。
代码
# 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#
题解
题意中给出的四个位置即为该位置行列循环意义下的邻居。
当 的时候直接第 列填 即可通过。注意到 ,这启发我们构造不会太复杂,可以直接在这种填法上面微调。
简单尝试后发现,某个位置的邻居在斜线上是相邻的。这启发我们把构造旋转 度,改为第 条对角线填上 。我们可以很自然地发现第 条对角线交错填 和 是可行的,于是我们解决了 的情形。对于 的情况是类似的,具体来说,如果 就交错填 和 ,否则直接填 即可。
代码
# 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#
题解
本题乍一看无从下手。不妨考虑子问题:如何询问得出这 个数的最大值编号?
我们可以用 次询问得到答案。具体地,我们给这 个人分为 组,查询组内的最大值编号,然后查询这 组的最大值编号。
现在,考虑下一个问题:如何询问得出次大值编号?
虽然最大值所在组已经不足 人,但是,我们还是可以用 次询问覆盖到所有的 个人,最后再查询一次。以此类推,可以发现,总查询次数为惊人的 。
我们发现,询问次大值时,对于之前信息的利用不够。只有最大值所在组的最大值编号会改变。而对于其它组,最大值编号是不变的。现在问题在于如何求出最大值所在组的最大值编号。
一个直接的想法是,直接拼一个其它组的元素。这是不可行的,就算该元素在自己的组内比较小,也有可能直接比最大值所在组的剩下 个元素都大。但是,当我们重新审视「分组」这一操作的时候,我们发现,这一轮的分组并不需要和上一轮保持一致。因此,我们可以采用以下策略:直接拼一个其它组的,不是所在组最大值的元素到当前组,然后查询。如果该元素是最大值,就直接将这个元素划分到当前组,并成为当前组的最大值。
这一流程显然是可以继续推广的。当剩余元素数量大于 的时候,我们采用以下策略:
- 将每个组的最大值编号拼到一次查询中,得到目前的最大值编号。
- 从最大值编号所在组中删除该元素。此时,该组可能会被删空。因为剩余元素数量大于 ,因此至少有 个元素没有成为所在组的最大值。我们总是可以将这些元素的若干个,和该组剩余元素,拼成一次询问,然后重新得到该组的最大值。这一过程可能会更改元素的分组。
这样,任意时刻每组都有至少一个元素。
当剩余元素数量等于 的时候,情况有一些不一样:现在在执行上面策略的第二步时,凑不齐 个元素进行一组询问了。我们注意到,现在只剩下 个没有成为所在组最大值的元素,但是总共也只剩下 个元素。合理猜测剩下的 个元素恰好是前 小。这也是容易证明的:
- 当一个元素成为组内最大值的时候,它必然不是前 小。
- 现在剩下 个元素,我们已知其中 个元素不是前 小,那么剩下的 个元素必然是前 小。
于是这一部分可以采用以下方法解决:
- 如果只剩下一个不是前 小的元素,则它是第 小,不需要任何询问。
- 否则,取出场上所有不是前 小的元素,然后拼上若干个前 小组成一次询问,询问的答案就是剩余元素最大值。然后删去该元素。
考虑总询问次数:
- 询问最大值:共 次。
- 询问中间的 个数:共 次。
- 询问最后 个数:共 次。
这样,我们的总询问次数恰好为 次,可以通过此题。
代码
# 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)
枚举每一个长度 ,长度为 的本质不同子串个数有两个上界: 和 。
直接加起来可以得到答案的显然上界:
当然,具体构造仍然是不太简单的。
我们感性理解,如果长度为 的 串出现了 个,那么长度为 的 串肯定也全出现了。同时,如果长度为 的所有 个子串都是不同的,那么长度为 的 串肯定也全部不同。
找到最大的 使得 ,此时,如果长度为 的 串全部出现过,长度为 的子串都是不同的,那么每一部分都取满上界。
考虑图论模型。构造有 个点的图 。图上的所有点恰好代表每一种长度为 的 串。若串 去掉首字符,并加上串 的末字符恰好就是串 ,则连边 。不难发现,每个点有两条入边两条出边,图中共 条边,恰好代表每一种长度为 的 串。
若能得出图 的哈密顿回路,则在 中删掉这些边后,剩下的图中每个点恰为一条出边一条入边,形成若干个环。
注意到哈密顿回路必然和任意环点有交。我们扭转交点处的两条出边,即可将该环插入到哈密顿回路所在的环中。我们添加若干个环直到边数不小于 。此时容易构造一条路径使得经过了哈密顿回路上的所有边,且总长度恰好为 。不难验证此时同时满足了关于 和 的两条限制。
关于图 的哈密顿回路求法:
- 直接搜索。证明留做习题。
- 注意到图 的哈密顿回路是图 的欧拉回路。跑欧拉回路即可。
代码
# 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#
题解
何时无解?感性理解,一个元素出现太多或者太少都是不好的。考虑 的情形,显然必须有 ,最优的排布方法肯定是 这样。如果一个 到上一个 和下一个 的中间都没有 那肯定不合法。
拓展到 更大的情形,设 分别是最小值和最大值。不难发现 这一条件仍然是必要的。下面证明这一条件充分:
- 按照每次在序列末尾追加元素的方式构造答案。
- 如果 ,则追加 ,并将所有 减去 ;
- 否则,将 划分为两个集合 ,其中若 则划分到集合 ,否则划分到集合 。按照 的顺序追加,并将 减去 , 减去 。
接下来仍然有难点:求字典序最小解。
枚举下一次覆盖结束点,即:枚举 ,并检查是否可以往当前序列末端添加一个长度为 的序列,使得添加完之后,新序列的末尾 个恰好是一个排列,同时保证接下来会有解。那么求出字典序最小的合法序列作为这一步添加的序列即可。特殊地,第一次只枚举 。
因为我们钦定了新序列的末尾 个是一个排列,所以当 固定的时候,要添加的元素集合是固定的。考察该集合是否能以任意顺序排列:
-
求出新的 数组,记作 。类似地定义 。
-
如果 ,那么接下来一定有解。因此可以将该集合从小到大排列,以保证字典序最小;
-
如果 ,那么根据和上面类似的证明,无论以什么顺序排布,接下来都一定无解;
-
如果 ,接下来是有解的!这和上面的情况不同。注意到,如果只有 这五个元素,不管怎么排布都是无解的。但是如果前面的序列末尾是 ,我们就可以把这五个元素安排成 。第一个 虽然没办法和后面的元素组成排列,但是可以和前面的序列末尾组成排列 。
所以这种情况下只是需要精心安排位置。观察发现,我们在后面的构造中,让出现次数等于 的元素能够「蹭到」该集合里面出现次数等于 的元素,于是该集合在排列的时候不能把出现次数等于 的元素排在出现次数等于 的元素后面。除此之外没有限制。据此贪心放置即可。
代码
# 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#
题解
仍然要先考虑无解的条件。感受一下,发现 ,输出是 行,这个数字恰好是 。猜一下发现有解的充要条件是:图存在完美匹配。
考虑证明。
- 如果图存在完美匹配,则删去所有匹配边,图中每个点度数变为 ,即图被分为若干个环。对环随意定向,并认为一条边被这条边的终点管辖。那么,找出每个匹配 ,则依次经过 的管辖边,, 的管辖边可以找到一条长度为 的路径。
- 如果存在解,那么取出每条路径中间两个点匹配,得到的必然是图的一组完美匹配。因为这一步中如果一个点被多次取出则度数至少为 。
因此,选用一种一般图最大匹配算法即可构造出答案。此处为随机化算法。
代码
# 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#
题解
转化为 行 列的二维前缀异或和数组 。矩形 的值的奇偶性等于 ,即选取两行两列的四个位置来异或。
根据均值不等式,那么我们希望两行异或得到的 列中,为 的列数量尽可能接近。
取 即可使得两行异或得到的为 的列数量完全相同。
代码
# 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#
题解
操作太多次肯定和操作 次等价。进一步发现本质不同的操作只有:
- 什么都不做;
- 做一次操作:U,D,L,R;
- 做两次操作:UL,UR,DL,DR,LU,LD,RU,RD。
更长的操作序列都可以简化。对于出现了多个 U/D,L/R 的情况,和只保留最后一次出现是等价的。据此得到 全部无解。利用单次 的暴力运行若干组 较小的数据后寻找规律即可。
代码 (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] 众生之门#
题解
打表发现答案不超过 。
进一步地,答案奇偶性固定,所以只需要检验答案是 还是 。
同时,我们还发现,取到最小值的解是不少的,解的大小可以近似看作在 内均匀分布。于是我们随机化 次就很可能就能找到一组解。猜测除了 比较小的时候,以及树是菊花的情况,答案全部都 。特判掉这些情况(如果 比较小,直接跑暴力;否则,怎么排列都是一样的)
当然,我们不能盲目蛮干。随机排列的复杂度肯定不能是 的,我们可以先随机一个排列,然后随机交换相邻两项。为了减少常数,记得使用 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#
题解
猜测树高只有 层,因为树的形态越复杂我们构造的时候越不好控制。
首先还是发现, 的时候是没有解的。另外,异或是有性质的:任意四个连续数的异或和都是 。这启发我们对这些数分成若干组,组间之间不干扰。
注意到把 当根的时候,对于每个 ,我们把 分成一组,然后构造一组 的链,最后把 挂到 的下面组成链 即可。
最后的问题是 的情况。我们判掉了 的情况,遂猜测 的连边可能跟 的 popcount 至少是 有点关系。事实也是如此,令 为 的 lowbit,则 也是偶数。构造链 即可。上述链不会破坏之前构造出的结构,因为 此时和编号小于 的偶数点、编号大于 小于 的奇数点都有连边。
代码
# 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 的技巧,首先考虑:什么时候无解?注意到交换要改变逆序对奇偶性,那么 如果不是偶数就无解了。因此 。
这个时候已经只剩下 的两种情况了。先来考虑 的情况。
一种合理的尝试是将元素按 个一组分组。因为具体是什么元素不重要,于是我们可以假装分组是 这样。
搜索发现存在一系列操作刚好消耗完组内的 个操作;同时,存在一系列操作能够消耗完两组间的所有 个操作(这个其实也是可以搜出来的,大致思路是:我们感知一下,可以把 个操作分成 组,使得每组内每个元素只参与恰好 次交换;这样搜索复杂度就大大减少了)。因此按照任意顺序执行组内 / 组外操作即可。
有了 的情况,对于 的情况,就不太困难了。我们只需要在适当的时候插入有 参与的操作就行了。具体地,对于一个组内操作 ,其中 ,我们可以等价替换为 。
代码
# 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. 归纳构造#
;;。
UOJ496 新年的新航线#
题解
(From KING_OF_TURTLE)
对于 的情形:
考虑多边形最外侧的三角形 ,设 是外侧点,可以发现与 相邻的两条边中必定删除恰好一条。
先删除这两条边,然后给三角形的内侧边 打上一个标记 ,表示之后 要向 两个点中的恰好一个连边。
接下来,我们每次考虑删除的时候,要考虑边上的标记。对于最外侧的三角形 ,设 为外侧的点,分情况讨论:
- 和 均无标记:将 打到 上;
- 和 中恰有一个有标记:设 有标记,把标记点和 都连到 上面;(因为 成为外侧点的时候还要连至少一条边所以是 OK 的)
- 和 均有标记:将标记点都连到 上,再把 打到 上。
简单讨论或直接搜索均可。
代码
# 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#
题解
「换斗移星,谋事在人」
对于 的情况是平凡的。对于 的情况目测无解。对于 的情况均无解,但 的情况均有解。
手玩应该是可以玩出来的,但是草稿纸弄掉了,火大。这里贴一个别人的构造吧:
在构造的过程中,我们尽可能把度数小于 的点往外放,力求增量构造的时候把之前度数小于 的点变为度数不小于 的点。尝试之后,发现可以放出来两个度数为 的点。因此,一种增量构造方法如下:
此时最上方和内部的点度数为 ,左右两角的点度数为 。
使用这种方法添加了 个点并保持了原来的结构。
因为我们有 的构造,可以按照这种方法构造出其余偶数的解。大胆猜测 是偶数都有解, 是奇数则无解。
代码略。
Codeforces 715D Create a Maze#
题解
注意到 的级别仅为 ,这启发我们要若干个乘 加 的操作。
用 实现乘 加 是简单的,如下图:
但是这道题限制相当紧,无法通过。我们发现我们对左侧的利用不够:左侧和上侧都可以利用,但是因为进制取得太小,导致左侧只能被完全封上。
尝试取 进行基本构造。
这样,组合三个 处的开 / 关,可以实现 完成乘 加 ,足以通过本题。
取其它进制或者进行合理的随机化也可以通过,可以自行查阅其它题解。
代码
# 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
把⼀个 个点的完全图的所有边排成一个环,使得任意连续的 条边都构成一棵树。
题 2.1
把一个 个点的完全图分成 组匹配, 为偶数。
题 2.2
把一个 个点的完全图分成 组哈密顿路, 为偶数。
题 2.3
把一个 个点的完全图分成 组哈密顿回路, 为奇数。
Part 6. 实战#
Codeforces 341E Candies Game#
题解
考虑有三堆大小分别为 的糖果时怎么做。
设 。对 二进制拆分,从低到高考虑 的每一位,设当前位位权为 :
- 若 的 位为 ,则执行操作 ,此时 的 位被夺去, 翻倍;
- 若 的 位为 ,则执行操作 (不难验证仍有 ),此时 翻倍。
那么操作后 变为 。排序后,新的 满足 。
次减少一堆,于是总次数大概是 的,可以通过。
代码
# 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#
题解
先判断奇偶性是否合法。
考虑进行坐标转换:。本质上是,先旋转坐标系使得两维度独立,再加上 使得坐标变化量非负。
这样,两维是独立的。接下来以 这一维举例。我们本质上想求一个周期内的位移 。为此,对于第 条信息 ,我们记录 ,。将信息按照 排序之后(因为有边界情况,我们加入信息 和 ),考虑相邻两项。
注意到一个周期的位移是 ,周期内 到 这段时间的位移可以是 ,那么我们有限制:
解不等式即可。
如果有解我们可以根据 和 还原出第 秒应该在哪个位置。那么有构造:如果当前还没走到那个位置,就增加坐标,否则在那个位置旁边横跳即可。如果奇偶性是正确的,这个办法总是可行。
代码
# 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#
题解
仍然是要先考虑判断无解的。将石子排序之后差分,不难观察到差分的 是不变的,于是,如果两个状态的差分 不同就无解。同时奇偶性也是不会变的,于是还要检查奇偶性。
否则石子的坐标总能够直接表示为 。接下来讨论所有石子的坐标均已经变化为 的情形,此时差分 是 。
注意到操作可逆。我们想办法从起始状态和终止状态出发,得到两个比较简单的状态,如果能够在这两个比较简单的状态之间移动,那么就解决了。
这里一个简单的状态是:位置的极差 。更进一步地,我们会移动到 处,其中 是一个偶数。考虑如何达到这个状态。
设石子的位置为 ,。如果 中间有某个 或者 ,那么将 中离这个 更近的那一个翻到另一侧去,可以让 变为原来的至多 。
否则,我们记 。此时都有 。考虑快速增大 使得其中某一个达到至少 ,就可以执行上面的操作了。
考虑如下操作:将 沿着 翻折, 会先变为 ,再变为 。那么 中较小的那个总能够通过这种方式来增加较大的那个的两倍。不难发现 次后就会变得符合条件。
设起始状态和终止状态分别移动到了 和 。我们希望将 移动到 处。不妨设 ,有一个简单的每次平移 的方法:执行两次「将 对着 翻折」的操作。此时操作次数太多,无法通过。
注意到每次移动的距离等于二倍极差。同时,执行「将 对着 翻折, 对着 翻折」的操作会使得极差变为原来的至少两倍。于是,我们可以采取这样的方法:先按照这种方法增大极差,直到极差即将超过 ,然后执行平移。平移后,如果当前极差已经超过 ,则撤销一次这种操作,减少极差。
这样总操作次数是 级别的,大部分时候不太满,可以通过此题。
代码
# 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#
题解
考虑对于一个给定的序列 ,判定是否合法:
- ;
- 对于任意 , 应该是偶数;
- 对于任意 , 应该恰好出现 种数;
- 对于每种数,第一次出现位置和最后一次出现位置构成的区间,要么包含要么不交。
现在可以开始做了。设我们在处理 这个区间,我们想要 刚好是一棵完整的子树。那么 必须成立(或者有 ),且 应该是偶数。接下来处理区间内部,我们遍历 每一个非零位置 ,并且找到下一个和 相等的位置 。如果 在区间外部那么已经非法,否则 也构成一棵完整的子树,我们就把 缩成一个点 ,然后找下一个可能存在的 。重复该过程,此时 内不会有相同数。
如果此时 中出现的种类数大于 ,已经无解;否则在区间开头的若干个 处填上新出现的数。接下来采用如下方式填上剩余的 :
- 首先,若连续三个位置形如 ,则变为 然后缩成单点 ;对于 同理。
- 因为此时 中已经填上了 个数,经过上面的步骤之后,必然不会有相邻的两个 ,此时给区间填上子树的根一定是合法的。
使用链表维护做到线性。
代码
# 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 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通