OI 中一些构造方法
QQ82272760 · · 个人记录
构造沙漠。
不只是构造题,还包括一些和构造有关的题目。
之后遇到别的方法还会继续更。
其实就是瞎写。
上下界构造
方法简述
一些题目要求构造方案让某个东西“最小”或“最大”,有些情况答案刚好就是理论的上下界。然后根据这些边界的求解办法不难得出构造。
例题
① [CF1667C] Half Queen Cover
就是放一些能覆盖一行一列一斜线的皇后使得棋盘的每个格子能被覆盖。
假设它的理论下界为
由于这个剩下的正方形共有
这个下界要求行与列不重复。考虑
然后对于另外两个余数就在右下角补上一两个皇后。
#include<iostream>
#include<cstdio>
using namespace std;
int n,k,k1,k2;
int main(){
scanf("%d",&n);
k=(n*2+1)/3;
printf("%d\n",k);
k1=(n-2)/3; k2=(n+1)/3;
for(int i=1;i<=k1;i+=1){
printf("%d %d\n",i,k1-i+1);
}
for(int i=1;i<=k2;i+=1){
printf("%d %d\n",k1+i,k1+k2-i+1);
}
for(int i=1;i<=k-k1-k2;i+=1){
printf("%d %d\n",n-i+1,n-i+1);
}
return 0;
}
② [CF1666E] Even Split
一条线段上面有
不难通过二分查找得出最大的最小值
构造的话先从左往右确定每个划分点的范围,即
不难证明
评测记录
配对
方法简述
比方说一些填数问题,就能将一些数两两配对放在一些同样被两两配对的位置。
例题
① [CF1670E] Hemose on the Tree
有一棵
其实也用到了『上下界构造』,它的下界是
如果第
#include<iostream>
#include<cstdio>
using namespace std;
int t,p,n;
int ansv[300005],anse[300005];
int tot,to[600005],nxt[600005],va[600005],lst[300005];
void add(int x,int y,int z){
to[++tot]=y;
nxt[tot]=lst[x];
lst[x]=tot;
va[tot]=z;
return;
}
void dfs(int x,int fa,int now){
int y,z;
for(int i=lst[x];i;i=nxt[i]){
if((y=to[i])==fa) continue;
z=va[i];
if(now) ansv[y]=z,anse[z]=z+n;
else ansv[y]=z+n,anse[z]=z;
dfs(y,x,now^1);
}
return;
}
void solve(){
int x,y;
scanf("%d",&p); n=(1<<p); tot=0;
for(int i=1;i<=n;i+=1) lst[i]=0;
for(int i=1;i<n;i+=1){
scanf("%d%d",&x,&y);
add(x,y,i); add(y,x,i);
}
ansv[1]=n; dfs(1,0,1);
printf("1\n");
for(int i=1;i<=n;i+=1) printf("%d ",ansv[i]);
printf("\n");
for(int i=1;i<n;i+=1) printf("%d ",anse[i]);
printf("\n");
return;
}
int main(){
scanf("%d",&t);
while(t--) solve();
return 0;
}
简化条件
方法简述
就是限制条件比较多,又或者是一个限制条件中涉及到的量比较多比较麻烦的题目,有时候有些条件是满足另一些条件后自动满足的,然后就可以少考虑一些条件。
例题
① [CF1734E] Rectangular Congruence
就是给一个
作为限制条件的四元组共有 SPJ 都不可能一个个检查,所以肯定有很多是没用的。
条件其实就是限制子矩阵的四个角,移一下项就是
由于最小的矩阵是
所以只要限制每个
剩下就只要确定第一行然后其他位置就唯一了。
#include<iostream>
#include<cstdio>
using namespace std;
int n;
int a[355][355];
int add(int x,int y){
if((x+=y)>=n) x-=n;
return x;
}
int sub(int x,int y){
if((x-=y)<0) x+=n;
return x;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i+=1) scanf("%d",&a[i][i]);
for(int i=2;i<=n;i+=1){
for(int j=i-1;j>=1;j-=1){
a[i][j]=sub(sub(add(a[i-1][j],a[i][j+1]),1),a[i-1][j+1]);
}
for(int j=i+1;j<=n;j+=1){
a[i][j]=sub(add(add(a[i-1][j],a[i][j-1]),1),a[i-1][j-1]);
}
}
for(int i=1;i<=n;i+=1){
for(int j=1;j<=n;j+=1){
printf("%d ",a[i][j]);
}
printf("\n");
}
return 0;
}
② [CF1615D] X(or)-mas Tree
给一棵树和一些限制条件,每个限制条件限制两个点路径上所有权值异或和中
这个限制条件太麻烦了,考虑把它简化掉。观察异或之后的 popcount 是如何改变的,可以发现 popcount 改变的奇偶性恰好就是新异或上的数的 popcount 的奇偶性。所以只要关注边权 popcount 的奇偶性就好了,每条边权变为
这个条件涉及到的变量还是比较多的,根据树上路径问题的经典套路,设
评测记录
分答案讨论
方法简述
有时候,答案的可能范围很小。不同于『上下界构造』,答案不一定刚好是上下界,因此做法是排除法式地检查每个答案。
例题
① [CF1689E] ANDfinity
两个元素连边当且仅当他们按位与的结果不为零,一次操作能让一个元素
首先要把 lowbit 最大的数,如果只有一个,减去
如果本来就连通答案就是下界;否则枚举变哪个数然后检查连通性;再不然答案就是上界。判断数的连通性可以转化为判断二进制位的连通性。
评测记录
② [CF1659E] AND-MEX Walk
给一张图,如果一条非简单路径(可以经过重复点边)经过的边权为
根据按位与的性质,不难发现集合中
如果答案为
如果答案为
其它情况一定是
#include<iostream>
#include<cstdio>
using namespace std;
int n,m,q,t=30;
int x,y,z;
int fa[30][100005];
int o[30][100005];
int tag[30][100005],vi[30][100005];
int find(int c,int x){
return fa[c][x]==x?x:fa[c][x]=find(c,fa[c][x]);
}
void merge(int c,int x,int y,int z){
x=find(c,x); y=find(c,y);
fa[c][x]=y;
o[c][y]&=(o[c][x]&z);
return;
}
int query(int x,int y){
for(int i=0;i<t;i+=1){
if(find(i,x)==find(i,y)) return 0;
}
for(int i=1;i<t;i+=1){
y=find(i,x);
if(!o[i][y]||tag[i][y]) return 1;
}
return 2;
}
int main(){
int x,y,z;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i+=1){
for(int j=0;j<t;j+=1){
fa[j][i]=i;
o[j][i]=1;
}
}
for(int i=1;i<=m;i+=1){
scanf("%d%d%d",&x,&y,&z);
for(int j=0;j<t;j+=1){
if(z>>j&1) merge(j,x,y,z&1);
else if(!(z&1)) vi[j][x]=vi[j][y]=1;
}
}
for(int i=1;i<=n;i+=1){
for(int j=0;j<t;j+=1){
if(vi[j][i]) tag[j][find(j,i)]=1;
}
}
scanf("%d",&q);
while(q--){
scanf("%d%d",&x,&y);
printf("%d\n",query(x,y));
}
return 0;
}
反向构造
方法简述
正着来不好想的题目就要反着想,大概就是构造出原问题的补集。
例题
① [CF1610E] AmShZ and G.O.A.T.
一个序列被称作毒瘤的当且仅当小于平均值的数的个数小于大于平均值的数的个数。要删掉尽可能少的数使得不存在子序列是毒瘤的。
根据『简化条件』的方法,先看看这个毒瘤序列有什么性质,它能不能被更小的毒瘤序列表示。手动模拟可以发现任意长度的毒瘤序列一定可以删剩一个长度为
考虑如何证明。采用反证法假设不存在,设最小值、次大值、最大值分别为
所以只要保证不存在任何长度为
但是如果考虑删除什么数是不好想的,因此要考虑保留什么数。首先确定最小值,等于最小值的位置全部都要选,然后再选一个严格次小值。根据贪心,这个严格次小值要尽可能小,接着再选后面的数,在满足条件的前提下也要尽量小。不难发现,每次新增一个数极差就会翻倍,所以最多也只有
所以枚举最小值后每次暴力用二分查找,最后取一个保留个数最大值就好了,复杂度
#include<iostream>
#include<cstdio>
#define inf 1145141919
using namespace std;
int t,n,ans;
int a[200005];
int find(int l,int lmt){
int r=n+1,mid;
while(l<r){
mid=l+r>>1;
if(a[mid]>=lmt) r=mid;
else l=mid+1;
}
return l;
}
int check(int x,int res){
int now=x+1;
if(now<=n) res+=1;
while((now=find(now+1,2*a[now]-a[x]))<=n) res+=1;
return res;
}
void solve(){
scanf("%d",&n); ans=inf;
for(int i=1;i<=n;i+=1) scanf("%d",&a[i]);
a[n+1]=inf;
for(int i=1,j=0;i<=n;i=j+1){
while(a[i]==a[j+1]) j+=1;
ans=min(ans,n-check(j,j-i+1));
}
printf("%d\n",ans);
return;
}
int main(){
scanf("%d",&t);
while(t--) solve();
return 0;
}
分组构造
方法简述
如果可以将要构造的东西分成若干组后,使得每组之间的构造方法互不影响,那就可以分开来构造。
例题
① [CF1628C] Grid Xor
本质上就是选一些能格子使得所有格子被覆盖奇数次,一个格子能覆盖上下左右四格,不包括自己。
这类棋盘问题很经典的套路就是黑白染色,然后发现白色的格子只能影响到黑色格,反之亦然,所以按照横纵坐标之和的奇偶性分类。
考虑与主对角线平行的同色斜线,在白色格子上放棋子会影响到旁边两条斜线,因此放棋子的白色斜线中间还要隔着一条白色斜线,同一条斜线上的相邻两个棋子中间也要隔着一个棋子,这样每个黑色格子都恰好被覆盖一次。对于黑色同理。
具体放法有其实有很多种,代码展示的是其中一种放法。
#include<iostream>
#include<cstdio>
using namespace std;
int t,n,ans,a,x,y;
void solve(){
scanf("%d",&n); ans=0;
for(int i=1;i<=n;i+=1){
for(int j=1;j<=n;j+=1){
scanf("%d",&a);
if((i+j)&1){
x=(i&3); y=(j&3);
if(i>=j){
if(x==2&&y==1) ans^=a;
if(x==0&&y==3) ans^=a;
}
else{
if(x==1&&y==0) ans^=a;
if(x==3&&y==2) ans^=a;
}
}
else{
x=((n-i+1)&3); y=(j&3);
if(n-i+1>=j){
if(x==2&&y==1) ans^=a;
if(x==0&&y==3) ans^=a;
}
else{
if(x==1&&y==0) ans^=a;
if(x==3&&y==2) ans^=a;
}
}
}
}
printf("%d\n",ans);
}
int main(){
scanf("%d",&t);
while(t--) solve();
return 0;
}
② [CF1607H] Banquet Preparations 2
给出若干三元组
考虑怎样的三元组才有可能得到相同的二元组,很显然是最后总和
事实上,每个
按照
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int t,n,ans,now;
int d[200005][2];
struct tri{
int id,a,b,m;
}p[200005];
int val(tri x){
return x.a+x.b-x.m;
}
int ri(tri x){
int a=x.a,b=x.b,m=x.m;
if(b>=m) return a;
else return a-(m-b);
}
bool operator<(tri x,tri y){
if(val(x)==val(y)){
return ri(x)<ri(y);
}
return val(x)<val(y);
}
void updt(tri x,int y){
int i=x.id,a=x.a,b=x.b,m=x.m;
d[i][0]=a-y,d[i][1]=m-(a-y);
return;
}
void solve(){
scanf("%d",&n); ans=0;
for(int i=1;i<=n;i+=1){
scanf("%d%d%d",&p[i].a,&p[i].b,&p[i].m);
p[i].id=i;
}
sort(p+1,p+n+1);
for(int l=1,r=0;l<=n;l=r+1){
while(r<n&&val(p[l])==val(p[r+1])) r+=1;
ans+=1; now=ri(p[l]); updt(p[l],now);
for(int i=l+1;i<=r;i+=1){
if(p[i].a-p[i].m>now){
ans+=1; now=ri(p[i]);
}
updt(p[i],now);
}
}
printf("%d\n",ans);
for(int i=1;i<=n;i+=1){
printf("%d %d\n",d[i][0],d[i][1]);
}
return;
}
int main(){
scanf("%d",&t);
while(t--) solve();
return 0;
}
Thanks~