专项训练 dp
作者在做题的时候深感自己dp水平的低下(几近为零),于是尝试逼迫自己搞懂每道题并写一点做题记录,本质上是为了避免自己成为只会抄题解的机器。。
把所有dp题单总结放一起,留给考前复习用。
2024.10 提高组专题dp4
首先,对于一个合法的序列f,若f+x为合法序列,那么f+x+x必然也为合法序列。
其次状态设计,设 \(f_{i,j,0/1}\) 表示序列的前 \(i\) 个,在后面放 \(j\) 种数可以变成合法的(注意这一位的设计会比较诡异,但是为了方便转移),0/1表示当前这个序列是否合法。
考虑转移,对于第 \(i+1\) 位进行分讨,如果当前序列是合法的,则有
若下一位合法:$$f_{i+1,j,1}+=f_{i+1,j,1}+f_{i,j,1}*j$$
若下一位不合法:$$f_{i+1,j+1,0}+=f_{i,j,1}*(m-j)$$
2025.1 提高dp
卡了将近4h,死活不会写转移......
容易发现,为了使整张图联通,我们需要添加的边数是一定的,为图上联通块的个数 \(-1\) 。
所以原式中 \(k*c\) 的值是不变的,我们只需要使 \(x^2+y^2\) 的值最小。有 \(x^2+y^2=(x+y)*(x-y)\) , \(x+y\) 不变,为图的大小,所以我们只需要最小化 \(x\) 和 \(y\) 的差即可。
因为最终要分成两个联通块,我们删掉得便一定是割边或我们加上的边。
考虑对原图进行边双缩点,缩完点后得到的图一定是一个森林,记 \(x\) 所在的联通块大小为 \(siz_x\) ,断开割边后两部分值一定是 \(siz_x\) 和 图的大小减去 \(siz_x\)。
考虑可行性dp。设 \(dp_{i,j,0/1}\) 表示第¥\(i\) 个联通块,其中一个联通块大小为 \(j\) ,是否断边。dp 值为 \(1\) 表示可行,\(0\) 为不可行。初始时 \(dp_{0,0,0}=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;
}
}