专项训练 dp

作者在做题的时候深感自己dp水平的低下(几近为零),于是尝试逼迫自己搞懂每道题并写一点做题记录,本质上是为了避免自己成为只会抄题解的机器。。

把所有dp题单总结放一起,留给考前复习用。

2024.10 提高组专题dp4

1.[PA2021] Od deski do deski

首先,对于一个合法的序列f,若f+x为合法序列,那么f+x+x必然也为合法序列。

其次状态设计,设 fi,j,0/1 表示序列的前 i 个,在后面放 j 种数可以变成合法的(注意这一位的设计会比较诡异,但是为了方便转移),0/1表示当前这个序列是否合法。

考虑转移,对于第 i+1 位进行分讨,如果当前序列是合法的,则有
若下一位合法:fi+1,j,1+=fi+1,j,1+fi,j,1j

若下一位不合法:fi+1,j+1,0+=fi,j,1(mj)


2025.1 提高dp

1.Min-Fund Prison (Medium)

卡了将近4h,死活不会写转移......

容易发现,为了使整张图联通,我们需要添加的边数是一定的,为图上联通块的个数 1

所以原式中 kc 的值是不变的,我们只需要使 x2+y2 的值最小。有 x2+y2=(x+y)(xy)x+y 不变,为图的大小,所以我们只需要最小化 xy 的差即可。

因为最终要分成两个联通块,我们删掉得便一定是割边或我们加上的边。

考虑对原图进行边双缩点,缩完点后得到的图一定是一个森林,记 x 所在的联通块大小为 sizx ,断开割边后两部分值一定是 sizx 和 图的大小减去 sizx

考虑可行性dp。设 dpi,j,0/1 表示第¥i 个联通块,其中一个联通块大小为 j ,是否断边。dp 值为 1 表示可行,0 为不可行。初始时 dp0,0,0=1

接下来状态转移,如果没有断开,则直接连到对应联通块即可。

若连向第二个联通块,则

dpi,j,0∣=dpi1,j,0

dpi,j,1∣=dpi1,j,1

若连向第一个联通块,则

dpi,j,0∣=dpi1,jsizrt,0

dpi,j,1∣=dpi1,jsizrt,1

若断边,则必然会分成两部分,一部分,则有

dpi,j,0∣=dpi1,j(sizrtsizx),0

dpi,j,1∣=dpi1,jsizx,1

注意这里使用或进行转移。

点击查看代码
/*注意到加边使其联通的边数其实是不变的,只需要最小化x^2+y^2即可
当原图是一个边双时无解
边双缩点,然后进行可行性dp
设dp[i][j][k]表示 前i个树 删/没删完边 后能否达到k
注意到边数很小,所以不需要优化吧......*/
#include<bits/stdc++.h> 
#define int long long 
using namespace std;
const int maxn=305; 
int T,n,m,c;
struct edge{
	int to,nxt;
}e[2*maxn];
int head[maxn],edgenum=1;
void add_edge(int u,int v){
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
int dp[maxn][maxn][3];

int dfn[maxn],low[maxn],tim,sum,siz[maxn],col[maxn];
stack<int> s;
void tarjan(int u,int fa){//边双缩点 	
	dfn[u]=low[u]=++tim;
	s.push(u);
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(!dfn[v]){
			tarjan(v,i);
			low[u]=min(low[u],low[v]); 
		}
		else if(i!=(fa^1)) low[u]=min(low[u],dfn[v]); 
	}
	if(low[u]==dfn[u]){
		sum++;
		int y;
		do{
			y=s.top();
			s.pop();
			siz[sum]++;
			col[y]=sum;
		}while(y!=u);
	}
}
vector<int> vec[maxn];
int rt[maxn],st[maxn];
bool vis[maxn];
void dfs(int u,int root){
	vis[u]=1;
	rt[u]=root;
	for(int v:vec[u]){
		if(vis[v]) continue;
		dfs(v,root);
		siz[u]+=siz[v];
	}
}
void clear(){
	memset(e,0,sizeof(e));
	memset(head,0,sizeof(head));
	memset(dfn,0,sizeof(dfn));
	memset(dp,0,sizeof(dp));
	memset(low,0,sizeof(low));
	memset(col,0,sizeof(col));
	memset(siz,0,sizeof(siz));
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++){
		for(int j=0;j<vec[i].size();j++) vec[i][j]=0;
	}
	tim=0,sum=0;
	edgenum=1;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>T;
	while(T--){
		cin>>n>>m>>c;
		clear();
		for(int i=1,x,y;i<=m;i++){
			cin>>x>>y;
			add_edge(x,y);
			add_edge(y,x);
		}
		for(int i=1;i<=n;i++){
			if(!dfn[i]) tarjan(i,0);
		}
		if(sum==1){
			cout<<-1<<endl;
			continue;
		}
		for(int i=1;i<=n;i++){
			for(int j=head[i];j;j=e[j].nxt){
				if(col[e[j].to]!=col[i]){
					vec[col[e[j].to]].push_back(col[i]);
					vec[col[i]].push_back(col[e[j].to]);
				}
			}
		}
		int cnt=0;
		for(int i=1;i<=sum;i++){
			if(!vis[i]){
				dfs(i,i);
				st[++cnt]=i;//共有cnt棵树 
			}
		}
		dp[0][0][0]=1;
		c=(cnt-1)*c;
		for(int i=1;i<=cnt;i++){
			int tot=siz[st[i]];
			for(int j=0;j<=n;j++){//枚举大小 
				if(j>=tot){
					dp[i][j][0]|=dp[i-1][j-tot][0];
					dp[i][j][1]|=dp[i-1][j-tot][1];
				}
				dp[i][j][0]|=dp[i-1][j][0];
				dp[i][j][1]|=dp[i-1][j][1];
				for(int k=1;k<=sum;k++){
					//k不属于第i块 
					if(rt[k]!=st[i] || k==st[i]) continue;
					if(j>=tot-siz[k]) dp[i][j][1]|=dp[i-1][j-tot+siz[k]][0];
					if(j>=siz[k]) dp[i][j][1]|=dp[i-1][j-siz[k]][0];
				}
			}
		}
		long long ans=1e17;
		for(int i=0;i<=n;i++){
			if(dp[cnt][i][0] || dp[cnt][i][1]){
				ans=min(ans,1ll*i*i+1ll*(n-i)*(n-i));
			}
		}
		cout<<ans+c<<endl;
	}
}
posted @   Aapwp  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
我给你一个没有信仰的人的忠诚
点击右上角即可分享
微信分享提示