构造题学习笔记

抽屉原理

在构造题中,若我们遇到了 n/k 这样的操作次数的时候,可以考虑将所有数划分为 k 个集合。这样,最小的那个集合的大小就一定小于等于 n/k 了。

CF1198C

给定一张有 3n 个点,m 条边的无向图。请找到一个大小为 n 的点独立集或边独立集。

n105,m5×105

思路

维护一个点集和边集。初始时点集为全集,边集为空。

遍历 m 条边,若其两端点都在点集中,则将两点删去,并将边加入边集。

点集的大小设为 x,边集的大小设为 y,那么边集中的点数为 2y,且与点集中的点无交集。满足 x+2y=3n,那么根据抽屉原理,minx,yn

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=5e5+10;
int n,m,x,y,u[N],v[N],id[N];bool del[N];
void solve()
{
	scanf("%d%d",&n,&m);x=3*n,y=0;for(int i=1;i<=3*n;i++) del[i]=false;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&u[i],&v[i]);if(del[u[i]]||del[v[i]]) continue;
		del[u[i]]=del[v[i]]=true;x-=2,id[++y]=i;
	}
	if(x>=n)
	{
		puts("IndSet");int cnt=0;
		for(int i=1;i<=3*n&&cnt<n;i++) if(!del[i]) printf("%d ",i),cnt++;
		puts("");
	}
	else
	{
		puts("Matching");
		for(int i=1;i<=n;i++) printf("%d ",id[i]);
		puts("");
	}
}
int main()
{
	int T;scanf("%d",&T);while(T--) solve();
	
	return 0;
}

CF1534D

这是一道交互题。

目标:你需要求出一棵树的形态。

询问:一个数 x,然后将得到 n 个数表示各个点到 x 的距离。限 n/2 次。

思路

不难发现,一个点和与其距离为 1 的点相邻。考虑询问出所有相邻的点得到树的形态。

首先询问根节点,将其余所有的节点分为两类,一类到根节点距离为偶数,另一类到根节点距离为奇数。根据抽屉原理,这两类节点的最小值不超过 n/2。那么只需对较小的那类节点进行询问,将距离为 1 的点连边即可。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N=2023;
int n,x[N],y[N],u[N],v[N],d[N],tot,tx,ty;
int main()
{
	cin>>n;cout<<"? 1"<<endl;fflush (stdout);
	for(int a,i=1;i<=n;i++)
	{
		cin>>a;if(i==1) continue;d[i]=a;
		if(a%2) x[++tx]=i;else y[++ty]=i;
	}
	if(tx>ty)
	{
		for(int i=2;i<=n;i++) if(d[i]==1) u[++tot]=1,v[tot]=i;
		swap(x,y),swap(tx,ty);
	}
	for(int i=1;i<=tx;i++)
	{
		cout<<"? "<<x[i]<<endl;
		fflush(stdout);
		for(int j=1;j<=n;j++)
		{
			int a;cin>>a;
			if(a==1) u[++tot]=x[i],v[tot]=j;
		}
	}
	cout<<"!"<<endl;
	for(int i=1;i<n;i++) cout<<u[i]<<" "<<v[i]<<endl;
	return 0;
} 

CF1450C

给定一张 nn 列的棋盘,每个格子可能是空的或包含一个标志,标志有 XO 两种。

如果有三个相同的标志排列在一行或一列上的三个连续的位置,则称这个棋盘是一个 胜局, 否则称其为 平局

在一次操作中,你可以将一个 X 改成 O,或将一个 O 改成 X

设棋盘中标志的总数为 k,你需要用不超过 k3 次操作把给定的局面变成平局。

n300

思路

注意到能够构成胜局的三个位置,它们的横纵坐标和模 3 的余数各不相同。考虑将所有格子依照模 3 的余数分为 3 类。

此时只需要保证其中两类格子互不相同,剩下一类格子不变,也就不会出现胜局了。根据这种思路,一共有 6 种修改方案。每一个包含标志的格子,更改其颜色的方案有两种,那么总的更改次数就是 2k。根据抽屉原理,其中最小的修改次数不超过 k3

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=310;
char a[N][N],s[N][N];int n,m;
bool check(char t[])
{
	int cnt=0;
	for(int i=1;i<=n;i++) 
	    for(int j=1;j<=n;j++)
	    {
	    	if(t[(i+j)%3]=='O'&&s[i][j]=='X') cnt++,a[i][j]='O';
	    	else if(t[(i+j)%3]=='X'&&s[i][j]=='O') cnt++,a[i][j]='X';
	    	else a[i][j]=s[i][j];
		}
	return cnt<=m/3;
}
void print(){for(int i=1;i<=n;i++,puts("")) for(int j=1;j<=n;j++) putchar(a[i][j]);}
void solve()
{
	scanf("%d",&n);m=0;for(int i=1;i<=n;i++) scanf("%s",s[i]+1);
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) m+=(s[i][j]!='.');
	if(check(".XO")) return print(),void();
	if(check(".OX")) return print(),void();
	if(check("X.O")) return print(),void();
	if(check("O.X")) return print(),void();
	if(check("XO.")) return print(),void();
	if(check("OX.")) return print(),void();
}
int main()
{
	int T;scanf("%d",&T);while(T--) solve();
	return 0;
}

归纳法

考虑找到有解的充要条件,然后试图将原问题转为规模减一的一定有解的子问题。

CF1470D

给你一张图,你需要给一些点染色,要求:

1.一条边两端不能都染色了。
2.仅保留有一端染色了的边,图仍然连通。

判断是否有解,若是给出构造

思路

假设当前已经将一个大小为 k 的点集合法染色。考虑将点 i 加入到点集中。

如果点集中存在一个点 j,满足点 j 与点 i 有边直接相连,且点 j 被染色了,那么直接将点 i 加入点中,仍然合法;

若点集中不存在这样的点 j,那么将点 i 染色后加入点集中,仍然合法。

按照这样的步骤逐步扩大点集,如果原图联通,那么必定有解。

同时为了保证新枚举的点与点集中的点有边直接相连,可以按照 dfs 枚举图上的所有点。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=3e5+10,M=6e5+10;
int h[N],tot,cnt,n,m,idx,q[N];bool vis[N],col[N];
struct edge{int v,nex;}e[M];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void dfs(int u)
{
	vis[u]=true,cnt++;bool used=0;
	for(int v,i=h[u];i;i=e[i].nex) if(vis[v=e[i].v]) used|=col[v];col[u]=used^1;tot+=col[u];
	for(int v,i=h[u];i;i=e[i].nex) if(!vis[v=e[i].v]) dfs(v);
}
void solve()
{
	scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) h[i]=col[i]=vis[i]=0;idx=tot=cnt=0;
	for(int u,v,i=1;i<=m;i++) scanf("%d%d",&u,&v),add(u,v),add(v,u);dfs(1);
	if(cnt<n) return puts("NO"),void();puts("YES");printf("%d\n",tot);
	for(int i=1;i<=n;i++) if(col[i]) printf("%d ",i);puts("");
}
int main()
{
	int T;scanf("%d",&T);while(T--) solve();
	return 0;
}

CF1515F

给定一张图,点带权,初始为 𝑎𝑖

对于一条边 (𝑢,𝑣),若 𝑎𝑢+𝑎𝑣𝑥,则可以把这两点缩到一起,新点权为 𝑎𝑢+𝑎𝑣𝑥

判断能否缩到一起,若能,给出方案。

思路

不难发现,本题有解的必要条件为:i=1nai(n1)×x

利用归纳法可以证明此条件为充分条件。

任意给出原图的一棵生成树,考虑生成树上的一条边 (u,v)

au+avx,那么直接合并,将原问题转化为规模为 n1 的子问题。根据归纳假设,结论成立;

au+av<x。将这条边断开,根据抽屉原理,两棵新生成的子树中必然有一棵满足上述条件。根据归纳假设,这棵树可以合并成一个节点。此时必然可以合并这条边。那么就可以转化为一个规模更小的子问题,根据归纳假设,结论成立。

于是就可以直接对原图 dfs,若当前节点连接的两条边可以合并,则直接合并,否则放在最后合并。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define LL long long
const int N=3e5+10,M=6e5+10;
int ans[N],hh,tt,h[N],idx,n,m,x;LL a[N],sum;bool vis[N];
struct edge{int v,id,nex;}e[M];
void add(int u,int v,int id){e[++idx]=edge{v,id,h[u]};h[u]=idx;}
void dfs(int u)
{
	vis[u]=true;
	for(int i=h[u];i;i=e[i].nex)
	{
		int v=e[i].v;if(vis[v]) continue;dfs(v);
		if(a[u]+a[v]>=x) a[u]+=a[v]-x,ans[hh++]=e[i].id;
		else ans[tt--]=e[i].id;
	}
}
int main()
{
	scanf("%d%d%d",&n,&m,&x);for(int i=1;i<=n;i++) scanf("%lld",&a[i]),sum+=a[i];
	for(int u,v,i=1;i<=m;i++) scanf("%d%d",&u,&v),add(u,v,i),add(v,u,i);
	if(sum<1ll*(n-1)*x) return puts("NO"),0;puts("YES");hh=1,tt=n-1;dfs(1);
	for(int i=1;i<n;i++) printf("%d\n",ans[i]);
	return 0;
}

[ARC122E] Increasing LCMs

你需要重排一个数组 A,使得其前缀 lcm 严格单调递增。

𝑛100

思路:

如果 Ai 可以放在数组的末尾,那么一定满足 LCMji(GCD(Ai,Aj))<Ai

对于这样的数,只要有解,那么 Ai 必然可以放到数组的最后面,证明如下:

假设存在一个 Ai,单独考虑它时,可以放在数组的最末尾。而有解时,只能放在数组中间的某一个位置 j,那么此时将 Ai 移动到数组的末尾,对于 1j1 位置上的数没有影响,而对于 j+1n 位置上的数,删去 Ai 后前缀 LCM 依旧单调递增。根据假设前提,Ai 可以放到最后一位,那么 Ai 放在数组末尾依旧是一组合法的解。故结论成立。

那么只需要倒序填数,每次找到一个可以填上的数就填上去。根据前面的结论,如果有解,那么这样做一定可以找到一组解。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
#define LL long long
LL a[N],ans[N];int n;bool vis[N];
LL gcd(LL a,LL b){return b==0ll?a:gcd(b,a%b);}
LL lcm(LL a,LL b){return a/gcd(a,b)*b;}
int main()
{
	scanf("%d",&n);for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	for(int i=n;i>=1;i--)
	{
		bool ok=false;
		for(int j=1;j<=n;j++)
		{
			if(vis[j]) continue;LL l=1;
			for(int k=1;k<=n;k++) if(k!=j&&!vis[k]) l=lcm(l,gcd(a[j],a[k]));
			if(l!=a[j]){ans[i]=a[j];ok=true;vis[j]=true;break;}
		}
		if(!ok) return puts("No"),0;
	}
	puts("Yes");for(int i=1;i<=n;i++) printf("%lld ",ans[i]);
	return 0;
}

DFS 树

对于一张无向图进行 dfs,可以得到一棵 dfs 树。

dfs 树有如下的性质:

  • 每条非树边 (u,v) 都连向子树中某个点,且一定具有祖先关系,称为返祖边

  • 子树之间不存在非树边

image

如上图所示,黑色边为树边,蓝色边为返祖边。而既不是树边也不是返祖边的红色边,在原图中是不存在的(根据 dfs 的性质)。

CF1391E

给出一张无向连通图,选择以下任意一个任务完成:

  • 找到图中一条至少包含 n2 个点的简单路径。
  • 找到图中偶数(至少 n2 )个点,且将它们两两配对。使满足任意两个点对包含的 4 个点的导出子图至多存在 2 条边。

思路:

对原图建出 dfs 树。

  • 若其深度大于等于 n2,那么直接找路径。

  • 若其深度小于 n2,那么就将同一层的点两两配对。每层至多留下一个节点没有被匹配,那么总匹配的点数一定大于等于 n2。同时根据 dfs 树的性质,同一层的节点在原图中没有边直接相连,且最多存在两条返祖边。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N=5e5+10,M=2e6+10;
int h[N],idx,n,m,fa[N],dep[N];
vector<int>g[N];
struct edge{int v,nex;}e[M];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void dfs(int u)
{
	g[dep[u]].push_back(u);
	for(int i=h[u];i;i=e[i].nex)
	{
		int v=e[i].v;if(fa[v]) continue;
		fa[v]=u,dep[v]=dep[u]+1,dfs(v);
	}
}
void solve()
{
	scanf("%d%d",&n,&m);idx=0;for(int i=1;i<=n;i++) h[i]=fa[i]=0,g[i].clear();dep[1]=1;fa[1]=-1;
	for(int u,v,i=1;i<=m;i++) scanf("%d%d",&u,&v),add(u,v),add(v,u);dfs(1);int id=1;
	for(int i=2;i<=n;i++) if(dep[i]>dep[id]) id=i;
	if(dep[id]>=(n+1)/2) 
	{
		puts("PATH");printf("%d\n",dep[id]);while(~id) printf("%d ",id),id=fa[id];puts("");
		return ;
	}
	puts("PAIRING");int cnt=0;for(int i=1;i<=dep[id];i++) cnt+=(g[i].size())/2;printf("%d\n",cnt);
	for(int i=1;i<=dep[id];i++)
		for(int j=0;j+1<g[i].size();j+=2) 
		    printf("%d %d\n",g[i][j],g[i][j+1]);
	puts("");
}
int main()
{
	int T;scanf("%d",&T);while(T--) solve();
	return 0;
}

转化到图

将我们要找的东西转为图上的一个环,欧拉回路;或者转为 2-sat 或二分图染色问题。

CF1270G

给你n个整数a1,a2an,第i个整数ai满足inaii1.

找到一个这些整数的一个非空子集,使得它们的和为0。

思路:

连边 i>iai,根据题意,iai[1,n]

可以形成至少一棵基环树,内圈上的环就是答案。

i 连向的边为 toi,那么有 i=toi=(iai),于是 ai=0

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e6+10;
int h[N],idx,n,a[N],fa[N],cnt[N];bool vis[N];
struct edge{int v,nex;}e[N];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void dfs(int u)
{
	vis[u]=true;
	for(int i=h[u];i;i=e[i].nex)
	{
		int v=e[i].v;if(!vis[v]) fa[v]=u,cnt[v]=cnt[u]+1,dfs(v);
		else
		{
			printf("%d\n",cnt[u]-cnt[v]+1);
			while(u!=v){printf("%d ",u);u=fa[u];} printf("%d\n",v);
		}
	}
}
void solve()
{
	scanf("%d",&n);for(int i=1;i<=n;i++) cnt[i]=h[i]=0;idx=0;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),add(i,i-a[i]),vis[i]=false;dfs(1);
}
int main()
{
	int T;scanf("%d",&T);while(T--) solve();
	return 0;
}
posted @   曙诚  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示