训练题目(杂项)

2023年

11.29 

1. P9027 [CCC2021 S5] Math Homework

构造题:先算出每个位置拥有的所有因子的最小公倍数,然后维护每一段区间的 gcd,由于 gcd 具有结合性,所以可以使用 ST 表来维护。最后再查询每一段的 gcd 是否符合原题给的 gcd

2. P9402 [POI2020-2021R3] Droga do domu

图论题:分层图 + dijstra

中间实现过程很麻烦,还要优化空间,真难调

3. P4306 [JSOI2010] 连通数

图论题:建反图 +~tarjan 缩点 + 在反图上跑拓扑。用 bitset<N> g[N] 来存储每个点之间是否可达,转移是否可达时写 g[v]~|=g[u],用起来更方便。最后统计答案时,若二者可达,则对答案的贡献时 siz[u]~*~size[v]

4. P6268 [SHOI2002] 舞会

图论题:二分图的最大匹配。  答案为 n- 最大匹配的边数

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int match[N],vis[N],n,m,col[N],g[N][N];
void get_color(int u,int c){
	col[u]=c;
	for(int i=1;i<=n;i++){
		if(!col[i]&&g[u][i])
			get_color(i,3-c);
	}
}
int find(int u){
	for(int i=1;i<=n;i++){
		if(!vis[i]&&g[u][i]){
			vis[i]=1;
			if(match[i]==-1||find(match[i])){
				match[i]=u;
				return 1;
			}
		}
	}
	return 0;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		u++; v++;
		g[u][v]=1; g[v][u]=1;
	}
	for(int i=1;i<=n;i++)
		if(!col[i]) get_color(i,1);
	int res=0;
	memset(match,-1,sizeof(match));
	for(int i=1;i<=n;i++){
		if(col[i]!=1) continue;
//		printf("%d\n",i);
		memset(vis,0,sizeof(vis));
		res+=find(i);
	}
//	printf("%d\n",res);
	printf("%d\n",n-res);
	return 0;
}
5. P4329 [COCI2006-2007#1] Bond

状压 DP:每一次状态统计出有几个 1 ,即有几个人,为 cnt,此时对于第 cnt 个人来说,他可以做的任务为那些位置为 1 的任务,并且这个第 cnt 个人也对应着我们读入时的第 cnt 这个人。仔细想想,会发现这样转移是把所有情况都涵盖进去了。

#include<bits/stdc++.h>
using namespace std;
const int N=22;
int n;
double a[N][N],f[1<<N];

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			scanf("%lf",&a[i][j]),a[i][j]*=0.01;
	f[0]=1;
	for(int i=0;i<(1<<n);i++){
		int cnt=0;
		for(int j=1;j<=n;j++)
			if(i&(1<<(j-1))) cnt++;
		for(int j=1;j<=n;j++)
			if(i&(1<<(j-1)))
				f[i]=max(f[i],f[i^(1<<(j-1))]*a[cnt][j]);
	}
	printf("%.6lf",f[(1<<n)-1]*100);
	return 0;
}
6. P4376 [USACO18OPEN] Milking Order G

图论题:二分 + 暴力建图 + 跑拓扑。每一遍拓扑,如果发现跑完拓扑之后仍然有点的 deg[i]~!=0,说明这个图存在环,不符合条件,接着二分。

在有向图中,拓扑可以用来判断是否存在环。

7. P3961 [TJOI2013] 黄金矿工

动态规划:有依赖的背包问题,可以作为一个模板

#include<bits/stdc++.h>
using namespace std;
const int N=210,TT=4e4+10;
struct node{
	int x,y,t,v;
	double k;
}a[N];
int n,f[TT],num[N],v[N][N],t[N][N],cnt,T;

bool cmp(node t1,node t2){
	if(t1.k==t2.k) return t1.y<t2.y;
	return t1.k<t2.k;
}
int main(){
	scanf("%d%d",&n,&T);
	for(int i=1;i<=n;i++){
		int x,y,t,v;
		scanf("%d%d%d%d",&x,&y,&t,&v);
		double k=x*1.0/y;
		a[i]={x,y,t,v,k};
	}
	sort(a+1,a+1+n,cmp);
	for(int i=1;i<=n;i++){
		if(i==1||a[i].k!=a[i-1].k)
			cnt++;
		if(!num[cnt]){
			num[cnt]++;
			v[cnt][num[cnt]]=a[i].v;
			t[cnt][num[cnt]]=a[i].t;
		}
		else{
			num[cnt]++;
			v[cnt][num[cnt]]=v[cnt][num[cnt]-1]+a[i].v;
			t[cnt][num[cnt]]=t[cnt][num[cnt]-1]+a[i].t;
		}
	}
	for(int i=1;i<=cnt;i++)
		for(int j=T;j>=t[i][1];j--){
			int maxn=f[j];
			for(int k=1;k<=num[i];k++)
				if(j>=t[i][k])
					maxn=max(maxn,f[j-t[i][k]]+v[i][k]);
			f[j]=maxn;
		}
	printf("%d\n",f[T]);
	return 0;
}
8. P2227 [HNOI2001] 洗牌机

找规律题。 我们多模拟几次,发现会有一个循环节,所以先找到循环节大小,之后总次数减去多个循环节后,再递推几次就ok了

9. P8744 [蓝桥杯 2021 省 A] 左孩子右兄弟

树形 DP:很简单

10. P3177 [HAOI2015] 树上染色

树形 DP:设计状态: f[i][j] 表示以 i 为根的子树中有 j 个黑点的收益最大值

11. P4362 [NOI2002] 贪吃的九头龙

树形 DPf[i][j][0/1] 表示以 i 为根的子树中有 j 个果子被最大的龙头吃掉,并且当前 i 这个点是否被吃掉的最小的“难受值”。

12. P1270 “访问”美术馆

树形 DPf[i][j] 表示以 i 为根的子树,花费了 j 秒,所可以得到的最大的画的数量

13. P4438 [HNOI/AHOI2018] 道路

树形 DPf[i][x][y] 表示当前点 i 到根节点之间的路径中,有 x 条公路没有翻新,有 y 条铁路没有翻新的最小的不便利值。这道题使用倒推,进行 dfs(1,0,0) ,之后输出为 f[1][0][0]

优化空间的技巧(很重要):

但这道题空间如果正常开的话会很大,会开 f[40000][40][40],所以会炸空间,要优化。

因为这道题是一个完全二叉树,所以每个父节点 u 只会被其两个子节点 v1,v2 更新,所以我们在用 v1,v2 更新完其父节点 u 后就可以扔了,所以我们可以使用类似时间戳的东西来给每个点重新赋一个编号。当每一个父节点被更新完后,可以直接 tim-=2; 意思是将其两个儿子直接扔了。

具体看代码:

int dfn[N],tim;
void dfs(int u,int x,int y){
	if(!s[u]){
		dfn[u]=++tim;
		for(int i=0;i<=x;i++)
			for(int j=0;j<=y;j++)
				f[dfn[u]][i][j]=c[u]*(a[u]+i)*(b[u]+j);
		return;
	}
	dfn[u]=++tim;
	dfs(s[u],x+1,y); dfs(t[u],x,y+1);
	for(int i=0;i<=x;i++)
		for(int j=0;j<=y;j++){
			int rt=dfn[u],ls=dfn[s[u]],rs=dfn[t[u]];
			f[rt][i][j]=min(f[ls][i+1][j]+f[rs][i][j],f[ls][i][j]+f[rs][i][j+1]);
		}
	tim-=2;
}
14. P7537 [COCI2016-2017#4] Rima
15. P4471 [BJWC2018] 词韵

14、15二者题目一样

16. P3174 [HAOI2009] 毛毛虫

14、15、16同一类型的树形 DP:都是找树中的 “最长链”

找最长链方法:设 f[u] 为以 u 为根的子树,最长的一条链的长度,我们统计答案时用 u 的两个儿子的 最大值、次大值 链来拼接更新答案,只有这样才不会漏掉答案。

以16题的代码为例:

void dfs(int u,int fa){
	int siz=g[u].size();
	int maxn=0,sec=0;
	for(auto v:g[u]){
		if(v==fa) continue;
		dfs(v,u);
		sec=max(sec,f[v]);
		if(sec>maxn) swap(sec,maxn);
	}
	if(fa!=0) siz--;
	ans=max(ans,maxn+sec+siz-min(siz,2)+1+(fa!=0));
	f[u]=maxn+siz-min(siz,1)+1;
}
17. P2279 [HNOI2003] 消防局的设立

树形 DP:这道题可以学到 解决半径为 k 的最小覆盖问题

设 dis[u] 表示离点 u 最近的点与之的最短距离,当前点为 u ,我们遍历其 k 层的父亲,用来更新 dis[u]

int p=u;
for(int i=1;i<=K;i++){
	p=fa[p];
	dis[u]=min(dis[u],dis[p]+i);
}

若最后 dis[u]>k ,则说明没有点可以覆盖到当前点 u,因此我们将 u 的向上第 k 层父亲 p 变为可以覆盖其他点的标记点,此时再将 p 向上 k 层父亲,共 k 个 父亲,将他们的 dis 值重新更新一遍。

遍历时,我们先从深度最深的点开始遍历,之后就是如上的更新 dis,每一次加入新的标记点时答案加一

这样复杂度为 O(N*K)

核心代码:

for(int i=1;i<=n;i++){
	int p=a[i].id,u=a[i].id;
	for(int j=1;j<=K;j++){
		p=fa[p];
		dis[u]=min(dis[u],dis[p]+j);
	}
	if(dis[u]>K){
		dis[p]=0;
		for(int j=1;j<=K;j++){
			p=fa[p];
			dis[p]=min(dis[p],j);
		}
	}
}
18. P3360 偷天换日

树形背包问题:第 12 题的延伸版,改一下代码就行

19. Vlad and the Mountains

Kruskal 重构树模板题:将两点之间的边权定义为 两点的点权最大值

20. P9745 「KDOI-06-S」树上异或

树形 DP + 将统计方案数字拆解成 64 位的二进制:

首先设 f(i,j,k) 在以 i 为根的子树中,对于每一种割边的方法,点 i 所在连通块异或出来的值在二进制表示下的第 j 位为 k 的情况下,其他连通块异或的乘积之和

转移时考虑两种状态:1. 将 v 所在连通块并入 u 所在连通块   2. 将 v 作为单独的连通块,不并入 u 所在连通块。

这题可以学习一下 拆位 做法,重要

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e5+10,mod=998244353;
int n,f[N][65][2],pow2[65];
ll a[N];
vector<int> g[N];
void dfs(int u){
	int tmp[64][2];
	for(auto v:g[u]){
		dfs(v);
		memcpy(tmp,f[u],sizeof(tmp));
		memset(f[u],0,sizeof(f[u]));
		int ans_v=0;
		for(int j=0;j<64;j++)
			ans_v=(ans_v+1ll*f[v][j][1]*pow2[j]%mod)%mod;
		for(int j=0;j<64;j++){
			for(int x=0;x<2;x++){
				f[u][j][x]=(f[u][j][x]+1ll*tmp[j][x]*ans_v%mod)%mod;
				for(int y=0;y<2;y++)
					f[u][j][x^y]=(f[u][j][x^y]+1ll*tmp[j][x]*f[v][j][y]%mod)%mod;
			}
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);	
	for(int i=2;i<=n;i++){
		int u;
		scanf("%d",&u);
		g[u].push_back(i);
	}
	for(int i=1;i<=n;i++)
		for(int j=0;j<64;j++)
			f[i][j][a[i]>>j&1]=1;
	pow2[0]=1;
	for(int i=1;i<=63;i++) pow2[i]=1ll*pow2[i-1]*2%mod;
	dfs(1);
	int ans=0;
	for(int j=0;j<64;j++)
		ans=(ans+1ll*f[1][j][1]*pow2[j]%mod)%mod;
	printf("%d\n",ans);
	return 0;
}
21. P3574 [POI2014] FAR-FarmCraft

树形 DP:设 f[u] 表示以 u 为根的子树中能玩到游戏的最晚时间,即取最大值。设 tim[u] 表示从 u 出发,将其子树中的所有点遍历完后又回到 u,总共花费的时间。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值