其他分享
首页 > 其他分享> > DP那些事

DP那些事

作者:互联网

一.基础DP:

一般的DP的步骤:

1.定义状态:找通解
(1)关注题目需要的东西

(2)状态确定,输出就确定

(3)将状态的准确描述记录下来

2.寻找状态转移方程:描述一个子问题如何用更小的子问题得到
3.确定边界条件:最小的子问题或不满足状态转移方程的状态

典型例题

例一 P1077 摆花

设 \(dp_{i,j}\) 表示前 i 种花一共摆了 j 盆时的种类数

然后让每一位加上前面可以达到的位的答案

答案即为 \(dp_{n,m}\)

Code

#include<bits/stdc++.h>
using namespace std;
int main()
{
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    dp[0][0]=1;
    for(register int i=1;i<=n;i++)
        for(register int j=0;j<=m;j++)
            for(register int k=0;k<=a[i];k++)
                if(j>=k)dp[i][j]=(dp[i][j]+dp[i-1][j-k])%MOD;//需要保证摆的花不超过j盆且不超过限制a[i]
    printf("%d",dp[n][m]);
    return 0;
}
例二 P1233 木棍加工

一眼可以看出是求下降子序列的个数。

下降子序列的个数等于最长上升子序列的长度。

Code

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e3+5;

struct Node
{
	int l,w;
}node[MAXN];

int n,ans;
int dp[MAXN];

inline bool cmp(Node a,Node b)
{
	if(a.l!=b.l)
		return a.l>b.l;
	return a.w>b.w;
}

int main()
{
	scanf("%d",&n);
	for(register int i=1;i<=n;i++)
	{
		scanf("%d%d",&node[i].l,&node[i].w);
	}
	sort(node+1,node+n+1,cmp);
	for(register int i=1;i<=n;i++)
	{
		dp[i]=1;
		for(register int j=1;j<=i-1;j++)
		{
			if(node[i].w>node[j].w)
			{
				dp[i]=max(dp[i],dp[j]+1);
			}
		}
	}
	int maxi=0;
	for(register int i=1;i<=n;i++)
		maxi=max(maxi,dp[i]);
		printf("%d",maxi);
	return 0;
}
例三 P1103 书本整理

不整齐度和剩下的书有关,所以状态和剩下的书有关。

设 \(dp_{i,j}\) 表示前 \(i\) 本书中留下了 $ j$ 本且第 \(i\) 本一定选的最小不整齐度。

寻找每一个可以与当前遍历到的 \(i\) 匹配的 \(p\) 即可。

Code

#include<bits/stdc++.h>
using namespace std;

struct Node
{
	int h,w;
}node[105];

inline bool cmp(Node x,Node y)
{
	return x.h<y.h;
}

int n,k;
int dp[105][105];

int main()
{
	scanf("%d%d",&n,&k);
	for(register int i=1;i<=n;i++)
	{
		scanf("%d%d",&node[i].h,&node[i].w);
	} 
	sort(node+1,node+1+n,cmp);
	for(register int i=1;i<=n;i++)
		for(register int j=1;j<=n-1;j++)
			dp[i][j]=1e9;
	for(register int i=1;i<=n;i++)
		dp[i][0]=dp[i][1]=0;
	for(register int i=1;i<=n;i++)
	 	for(register int j=1;j<=n-k;j++)
	 		for(register int p=j-1;p<=i-1;p++)
	 			dp[i][j]=min(dp[i][j],(dp[p][j-1]+abs(node[p].w-node[i].w)));
	int mini=1e9;
	for(register int i=n-k;i<=n;i++)
		mini=min(mini,dp[i][n-k]);
	printf("%d",mini);
	return 0;
}
例四 CF577B Modulo Sum

直接搞DP是 \(O(nm)\) 的,肯定会炸。

考虑一个小优化:当 \(n≥m\) 时必定有解。

我们假设 \(dp_{i,j}\) 表示考虑在前 i 个数中选数,是否可能使得它们的和除以 $m $ 的余数为 \(j\) ,初始状态 \(dp_{i,ai} =1\),枚举每个数和余数进行转移即可。

Code

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+5;
int n,m;
bool dp[MAXN][MAXN];
int a[MAXN];
int main()
{
    scanf("%d%d",&n,&m);
    if(n>m)
    {
        puts("YES");
        return 0;
    }
    for(register int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i]%=m;
        dp[i][a[i]]=1;
        if(!a[i])
        {
            puts("YES");
            return 0;
        }
    }
    for(register int i=1;i<=n;i++)
    {
        for(register int j=0;j<m;j++)
        {
            dp[i][j]|=dp[i-1][j];
            dp[i][(j+a[i])%m]|=dp[i-1][j];
        }
        if(dp[i][0])
        {
            puts("YES");
            return 0;
        }
    }
    puts("NO");
    return 0;
}
例五 P2196 挖地雷

有很多方法,比如暴搜。

可能第一印象是拓扑+DP

手玩可以发现,一定是从编号小的地窖跑到编号较大的地窖。

所以二维循环即可。

Code

#include <bits/stdc++.h>
using namespace std;

int n,a,y,x;
int d[30],w[30],id[30],nxt[30];
bool linker[30][30];

int count(int u)
{
	if(d[u])return d[u];
	int v=0,k=-1;
	for(int i=1;i<=n;i++)
		if(linker[u][i])
		{
			if(d[i]==0)d[i]=count(i);
			if(d[i]>v)v=d[i],k=i;
		}
	nxt[u]=k;
	return v+w[u];
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>w[i];
		id[i]=0;
		nxt[i]=-1;
	}
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
		{
			cin>>a;
			if(a)linker[i][j]=true,id[j]++;
		}
	for(int i=1;i<=n;i++)
		if(id[i]==0)
		{
			d[i]=count(i);
			if(d[i]>y)y=d[i],x=i;
		}
	while(x!=-1)
	{
		cout<<x<<' ';
		x=nxt[x];
	}
	cout<<endl<<y;
	return 0;
}

二.区间DP

1.定义

一般是求一个区间内的最大值,最小值,方案数。

2.判别:

(1)从不同位置开始递推得到的结果可能不一样。

(2)合并类或拆分类。

3.循环:

区间DP一般有三个循环

(1)第一个循环一般是枚举阶段(子问题)

(2)第二个循环枚举所有的状态(情形)

(3)第三个循环枚举决策点(从哪里转移)

典型例题

例一 P1775 石子合并(弱化版)

最经典的区间DP题,没什么好说

Code

#include <bits/stdc++.h>
using namespace std;
const int MAXN=305;

int n;
int a[MAXN];
int dp[MAXN][MAXN];
int sum[MAXN];

int main()
{
	scanf("%d",&n);
	memset(dp,0x7f,sizeof dp);
	for(register int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		sum[i]=sum[i-1]+a[i];
		dp[i][i]=0;
	}
	for(register int len=2;len<=n;len++)
		for(register int i=1;i+len-1<=n;i++)
		{
			int j=i+len-1;
			for(register int k=i;k<j;k++)
				dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1],dp[i][j]);
		}
	printf("%d",dp[1][n]);
	return 0;
}
例二 P3146 248 G

还是区间DP板子。

只要两边相等,就可以将两边合并。

Code

#include <bits/stdc++.h>
using namespace std;

int n;
int a[255];
int dp[255][255];
int ans;

int main()
{
	scanf("%d",&n);
	memset(dp,-0x7f,sizeof dp);
	for(register int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		dp[i][i]=a[i];
	}
	for(register int len=2;len<=n;len++)
		for(register int i=1;i<=n-len+1;i++)
		{
			int j=i+len-1;
			for(register int k=i;k<j;k++)
			{
				if(dp[i][k]==dp[k+1][j])dp[i][j]=max(dp[i][j],dp[i][k]+1);
			}
		}
	for(register int i=1;i<=n;i++)
		for(register int j=i+1;j<=n;j++)
			ans=max(ans,dp[i][j]);
	printf("%d",ans);
	return 0;
}
例三 P2890 Cheapest Palindrome G

注意转移就行......

Code

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
int n,m;
string a;
int dp[MAXN][MAXN];
map<char,int>ins,del;
signed main()
{
	scanf("%d%d",&n,&m);
	cin>>a;
	a=' '+a;
	memset(dp,0x7f,sizeof dp);
	for(register int i=1;i<=n;i++)
	{
		char op;
		int in,de;
		cin>>op>>in>>de;
		ins[op]=in;
		del[op]=de;
	}
	for(register int i=1;i<=m;i++)
		dp[i][i]=0;
	for(register int len=2;len<=m;len++)
		for(register int i=1;i+len-1<=m;i++)
		{
			int j=i+len-1;
			if(a[i]==a[j])
			{
				if(len==2)dp[i][j]=0;
				else dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
			}
			dp[i][j]=min(dp[i][j],dp[i][j-1]+ins[a[j]]);
			dp[i][j]=min(dp[i][j],dp[i][j-1]+del[a[j]]);
			dp[i][j]=min(dp[i][j],dp[i+1][j]+ins[a[i]]);
			dp[i][j]=min(dp[i][j],dp[i+1][j]+del[a[i]]);
		}
	printf("%d",dp[1][m]);
	return 0;
}
例四 P3205 合唱队

首先套路设 \(dp_{i,j}\) 为区间 \([i,j]\) 的方案数。

然后发现状态设计有问题。

所以再加一维表示最后一个是从哪边进来的。

有一个小坑点:只有一个人时从左边右边进来都一样,只要初始化一边即可。

Code

#include <bits/stdc++.h>
using namespace std;
const int MAXN=2010;
const int MOD=19650827;

int n;
int a[MAXN],dp[MAXN][MAXN][2];

int main()
{
	scanf("%d",&n);
	for(register int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(register int i=1;i<=n;i++)
		dp[i][i][1]=1;
	for(register int len=1;len<=n;len++)
	{
		for(register int i=1,j=i+len;j<=n;i++,j++)
		{
			if(a[i]<a[i+1])
				dp[i][j][0]+=dp[i+1][j][0]%MOD;
			if(a[i]<a[j])
				dp[i][j][0]+=dp[i+1][j][1]%MOD;
			if(a[j]>a[i])
				dp[i][j][1]+=dp[i][j-1][0]%MOD;
			if(a[j]>a[j-1])
				dp[i][j][1]+=dp[i][j-1][1]%MOD;
		}
	}
	printf("%d",(dp[1][n][0]+dp[1][n][1])%MOD);
 	return 0;
}
例五 P1220 关路灯

有了上一题的经验,一上来就可以设 \(dp_{i,j,0/1}\) 表示在区间 \([i,j]\) 内最后一个是 \(i/j\) 的最小代价。

转移也十分套路,按照思路模拟即可。

需要注意的是初始化。

因为给定了初始位置为 c 。

所以 $ dp_{i,i}$ \(=\) \(abs(a_c-a_i)\) \(*(sum_n-w_c)\)

Code

#include <bits/stdc++.h>
using namespace std;
const int MAXM=60;

int a[MAXM],b[MAXM],sum[MAXM],n,m,c;
int dp[MAXM][MAXM][2];

int main()
{
    scanf("%d%d",&n,&c);
    memset(dp,0x3f,sizeof(dp));
    for(register int i=1;i<=n;i++)
    {
    	scanf("%d%d",&a[i],&b[i]);
		sum[i]=sum[i-1]+b[i];
	}
    for(register int i=1;i<=n;i++)
    	dp[i][i][0]=dp[i][i][1]=abs(a[i]-a[c])*(sum[n]-b[c]);
    for(register int len=2;len<=n;len++)
        for(register int i=1;i+len-1<=n;i++)
        {
      	    int j=i+len-1;
      	    dp[i][j][0]=min(dp[i+1][j][0]+(a[i+1]-a[i])*(sum[i]+sum[n]-sum[j]),dp[i+1][j][1]+(a[j]-a[i])*(sum[i]+sum[n]-sum[j]));
    	    dp[i][j][1]=min(dp[i][j-1][0]+(a[j]-a[i])*(sum[i-1]+sum[n]-sum[j-1]),dp[i][j-1][1]+(a[j]-a[j-1])*(sum[i-1]+sum[n]-sum[j-1]));
        }
    int ans=min(dp[1][n][0],dp[1][n][1]);
    printf("%d",ans);
    return 0;
}

三.树形DP:

1.定义:

在一颗树形结构上做DP,一般题目会给一颗树形结构

2.步骤:

(1)确定根节点做dfs

(2)设计好状态

(3)找好状态从哪里转移过来,根据题目考虑清楚递归和转移的顺序

(4)推出状态转移方程并转移

(5)在主函数里做dfs即可

3.大概思路

其实本质上跟普通DP一样,就是将大问题转化为多个子问题并递归求解,只不过是在一棵树上做DP,所以要注意的点不同

典型例题

例一 P2015 二叉苹果树

Code

#include<include/bits/stdc++.h>
using namespace std;

const int N = 105; 
int n, q;
int dp[N][N];

struct node
{
	int to, w;
};
vector<node> nbr[105];

inline int dfs(int cur, int fa)
{
	int sum = 0; //求cur的子树的大小 
	for(int i = 0; i < nbr[cur].size(); i++) //物品 
	{
		int nxt = nbr[cur][i].to;
		int w = nbr[cur][i].w;
		if(nxt == fa)
			continue;
		sum = sum + dfs(nxt, cur) + 1;
		for(int v = min(sum, q); v >= 0; v--) //容量 
		{
			for(int k = 0; k <= v-1; k++) //孩子结点的保留的边的数量 
			{
				dp[cur][v] = max(dp[cur][v], dp[cur][v-k-1] + dp[nxt][k] + w);
			}
		}
	}
	return sum;
}

int main()
{
    cin >> n >> q;
    for(int i = 1; i <= n-1; i++)
    {
    	int x, y, w;
    	cin >> x >> y >> w;
    	nbr[x].push_back((node){y, w});
    	nbr[y].push_back((node){x, w});
	}
    dfs(1, 0);
    cout << dp[1][q]; //dp[q] 
    return 0;
}

可以说是最经典,最基础的树形DP题,由于是第一篇,所以写下注释......

例二 P1131 时态同步

Code

#include <bits/stdc++.h>
#define maxn 504561
using namespace std;
int head[maxn*2];
long long a[maxn*2];
int n,m,s,num;
long long ans=0;
bool vis[maxn];
struct point
{int to,next,dis;}e[maxn*2];
void add(int from,int to,int dis)
{
    e[++num].next=head[from];
    e[num].to=to;
    e[num].dis=dis;
    head[from]=num;
}
void dfs(int u)
{
    vis[u]=1;
    int cnt=0;
    for(int i=head[u];i!=0;i=e[i].next)
    {
        int to=e[i].to;
        if(!vis[to])
        {
                dfs(to);
               if(a[to]+e[i].dis>a[u])
               {
                   ans+=(a[to]+e[i].dis-a[u])*cnt;
                   cnt++;
                   a[u]=a[to]+e[i].dis;
               }
               else
               {
                   ans+=a[u]-a[to]-e[i].dis;
                   cnt++;
               }
        }
    }
}
int main()
{
    scanf("%d%d",&n,&s);
    for(int i=1;i<n;i++)
    {int x,y,z;
    scanf("%d%d%d",&x,&y,&z);
    add(x,y,z);add(y,x,z);}
    memset(vis,0,sizeof(vis));
    dfs(s);
    printf("%lld",ans);
    return 0;
}

普通树形DP......

例三 P1272 重修道路

Code

#include<include/bits/stdc++.h> 
using namespace std;
const int N = 155; 

int n, p;
int dp[N][N], in[N], out[N]; 
vector<int> nbr[155];

inline int dfs(int cur)
{
	int sum = 1; 
	for(int i = 0; i < nbr[cur].size(); i++) 
	{
		int nxt = nbr[cur][i];
		int tmp = dfs(nxt);
		sum = sum + tmp;
		for(int v = min(sum, p); v >= 2; v--) 
			for(int k = 1; k <= min(tmp, v-1); k++)
				dp[cur][v] = min(dp[cur][v], dp[cur][v-k] + dp[nxt][k] - 1);
	}
	return sum;
}

int main()
{
	int root;
    cin >> n >> p; 
    memset(dp, 0x3f, sizeof(dp)); 
    for(int i = 1; i <= n-1; i++)
    {
    	int x, y;
    	cin >> x >> y;
    	nbr[x].push_back(y);
    	out[x]++;
	}
	for(int i = 1; i <= n; i++) 
		dp[i][1] = out[i]; 
    dfs(1);
    int ans = dp[1][p];
    for(int i = 2; i <= n; i++)
    	ans = min(ans, dp[i][p] + 1);
    cout << ans;
    return 0;
}

普通树形DP,不说,看代码......

例四 P2014 选课

Code

#include <bits/stdc++.h>
#define maxn 1000
using namespace std;
int n,m,f[maxn][maxn],head[maxn],cnt;
struct edge
{
    int to,pre; 
}e[maxn];

inline int read()
{
    char a=getchar();
    while(a<'0'||a>'9')
    {
        a=getchar();
    }
    int t=0;
    while(a>='0'&&a<='9')
    {
        t=(t<<1)+(t<<3)+a-'0';
        a=getchar();
    }
    return t;
}

void add(int from,int to)
{
    e[++cnt].pre=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}

void dp(int now)
{
    for(int i=head[now];i;i=e[i].pre)
    {
        int go=e[i].to;
        dp(go);
        for(int j=m+1;j>=1;j--)
        {
            for(int k=0;k<j;k++)
            {
                f[now][j]=max(f[now][j],f[go][k]+f[now][j-k]);
            }
        }
    }
}

int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;i++)
    {
        int fa=read();
        f[i][1]=read();
        add(fa,i);
    }
    dp(0);
    printf("%d\n",f[0][m+1]);
    return 0;
}

这也没什么好说的,就是普通树形DP......

例五 P5658 括号树

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+5;

struct node
{
	int to,nxt;
}e[MAXN];

int n;

int head[MAXN],cnt;

int ye[MAXN],sum[MAXN],fa[MAXN];

string s;

stack<int>q;

inline void add(int x,int y)
{
	e[++cnt].to=y;
	e[cnt].nxt=head[x];
	head[x]=cnt;
}

inline void dfs(int x)
{
	bool flag=0;
	int tmp;
	if(s[x]==')')
	{
		if(!q.empty())
		{
			flag=1;
			tmp=q.top();
			ye[x]=ye[fa[q.top()]]+1;
			q.pop();
		}
	}
	else if(s[x]=='(')q.push(x);
	sum[x]=sum[fa[x]]+ye[x];
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to;
		dfs(y);
	}
	if(flag!=0)q.push(tmp);
	else if(!q.empty())q.pop();
}

signed main()
{
	scanf("%lld",&n);
	cin>>s;
	s='-'+s;
	for(register int i=2;i<=n;i++)
	{
		int f;
		scanf("%lld",&f);
		add(f,i);
		fa[i]=f;
	}
	dfs(1);
	int ans=0;
	for(register int i=1;i<=n;i++)
		ans^=sum[i]*i;
	printf("%lld",ans);
	return 0;
}

明显括号匹配问题都要用栈好吗......

这道题用到一些小技巧,与普通树形DP略有区别,但思路相同

其实树形DP不难,将本质搞清楚,多写题,将模式弄清楚就真的不难

四.换根DP:

1.定义:

对于一类树形DP,若节点不确定,且答案会随着根节点的不同而变换,这种树形DP可以称之为换根DP

在题目没有指明根节点的情况下在树上做DP,需要有两个dfs,一个普通树形DP,一个用来切换根节点求解

2.状态:

一般的,定义 \(dp_i\)表示以\(i\)为根节点的子树求出的子问题的答案,定义\(f_i\)为以\(i\)为全局根节点的子树求出的子问题的答案

3.状态转移:

一般做两遍dfs

dfs1中做普通树形DP,通过递归求解\(dp_i\)的值,需先递归再转移

dfs2中通过递归来求解换根DP\(f_i\)的值,需先转移再递归

且一般令\(dp_i\)=\(f_i\)

4.注意事项:

换根DP转移时需考虑清楚更换根节点后有哪些变化,增加了哪一部分,减少了哪一部分,把这些想清楚后再来规划转移方程

典型例题

例一 P3478 STA-Station

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e6+5;

struct node
{
	int nxt,to;
}e[MAXN];

int n;
int head[MAXN],cnt;

inline void add(int x,int y)
{
	e[++cnt].to=y;
	e[cnt].nxt=head[x];
	head[x]=cnt;
}

int dep[MAXN],siz[MAXN];

inline void dfs1(int x,int fa)
{
	dep[x]=dep[fa]+1;
	siz[x]=1;
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to;
		if(y==fa)
			continue;
		dfs1(y,x);
		siz[x]+=siz[y];
	}
}

int f[MAXN];

inline void dfs2(int x,int fa)
{
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to;
		if(y==fa)
			continue;
		f[y]=f[x]+n-siz[y]*2;
		dfs2(y,x);
	}
}

signed main()
{
	scanf("%lld",&n);
	for(register int i=1;i<=n-1;i++)
	{
		int x,y;
		scanf("%lld%lld",&x,&y);
		add(x,y);
		add(y,x);
	}
	dfs1(1,0);
	for(register int i=1;i<=n;i++)
		f[1]+=dep[i];
	dfs2(1,0);
	int root=1,maxn=0;
	for(register int i=1;i<=n;i++)
	{
		if(maxn<f[i])
		{
			maxn=f[i];
			root=i;
		}
	}
	printf("%lld",root);
	return 0;
}

换根DP的基础题,很简单......

例二 P2986 Great Cow Gathering G

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5;
const int MAXM=1e5+5;

struct node
{
	int nxt,to,len;
}e[MAXN];

int n,a[MAXM],b[MAXM],l[MAXM],c[MAXM];
int head[MAXN],cnt;
int sum;

inline void add(int x,int y,int z)
{
	e[++cnt].to=y;
	e[cnt].len=z;
	e[cnt].nxt=head[x];
	head[x]=cnt;
}

int dp[MAXN],f[MAXN],siz[MAXN];

inline void dfs1(int x,int fa)
{
	siz[x]=c[x];
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to,z=e[i].len;
		if(y==fa)
			continue;
		dfs1(y,x);
		siz[x]+=siz[y];
		dp[x]=dp[x]+dp[y]+siz[y]*z;
	}
}

inline void dfs2(int x,int fa)
{
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to,z=e[i].len;
		if(y==fa)
			continue;
		f[y]=f[x]+(sum-siz[y])*z-siz[y]*z;
		dfs2(y,x);
	}
}

signed main()
{
	scanf("%lld",&n);
	for(register int i=1;i<=n;i++)
	{
		scanf("%lld",&c[i]);
		sum+=c[i];
	}
	for(register int i=1;i<=n-1;i++)
	{
		scanf("%lld%lld%lld",&a[i],&b[i],&l[i]);
		add(a[i],b[i],l[i]);
		add(b[i],a[i],l[i]);
	}
	dfs1(1,0);
	f[1]=dp[1];
	dfs2(1,0);
	int minn=0x7f7f7f7f7f7f;
	for(register int i=1;i<=n;i++)
		minn=min(minn,f[i]);
	printf("%lld",minn);
	return 0;
}

很基础的一道换根DP,挺水的,只需要搞清楚状态从哪里转移,考虑全面就行了

例三 POJ3585 Accumulation Degree

Code

#include<bits/stdc++.h>
using namespace std;

int dp[200010], f[200010], deg[200010];
bool vis[200010];
int head[200010], to[400010], w[400010], nxt[400010];
int n, T, tot, root, ans;

inline void add(int x, int y, int z) 
{
	to[++tot] = y;
	w[tot] = z;
	nxt[tot] = head[x];
	head[x] = tot;
	return ;
}

inline void dfs1(int x) 
{
	vis[x] = true; 
	dp[x] = 0;  
	for(int i = head[x]; i; i = nxt[i]) 
	{ 
		int y = to[i];
		if(vis[y]) 
			continue;
		dfs1(y);
		if(deg[y] == 1) 
			dp[x] += w[i];
		else 
			dp[x] += min(dp[y], w[i]); 
	}
}

inline void dfs2(int x) 
{
	vis[x] = true;
	for(int i = head[x]; i; i = nxt[i])
	{
		int y = to[i];
		if(vis[y]) 
			continue;
		if(deg[x] == 1) 
			f[y] = dp[y] + w[i];
		else
			f[y] = dp[y] + min(f[x] - min(dp[y], w[i]), w[i]);
		dfs2(y);
	}
	return ;
}

int main() 
{
	cin >> T;
	while(T--) 
	{
		tot = 1;
		cin >> n;
		for(int i = 1; i <= n; i++)
			head[i] = f[i] = dp[i] = deg[i] = vis[i] = 0;
		for(int i = 1; i <= n-1; i++) 
		{
			int x, y, z;
			cin >> x >> y >> z;
			add(x, y, z);
			add(y, x, z);
			deg[x]++;
			deg[y]++;
		}
		int root = 1; 
		dfs1(root);
		memset(vis, 0, sizeof(vis));
		f[root] = dp[root];
		dfs2(root);
		int ans = 0;
		for(int i = 1; i <= n; i++)
			ans = max(ans, f[i]);
		cout << ans << endl;
	}
	return 0;
}

这道题主要是要考虑叶子结点,叶子节点必须设为极大值,且用度数统计判叶子节点的话要注意是否为只有一个子节点的根节点,若是则不能设为极大值

例四 CF1187E Tree Painting

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=200005;
struct node
{
	int to,nxt;
}e[MAXN<<1];
int n,cnt;
int head[MAXN],dp[MAXN],siz[MAXN],f[MAXN];
int ans,rea;
inline void up(int x,int y)
{
	dp[y]=dp[x]+n-siz[y]*2;
}
inline void add(int x,int y)
{
	e[++cnt].to=y;
	e[cnt].nxt=head[x];
	head[x]=cnt;
}
inline void dfs1(int x,int fa)
{
	siz[x]=1;
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to;
		if(y==fa)continue;
		dfs1(y,x);
		siz[x]+=siz[y];
		f[x]+=f[y];
	}
	f[x]+=siz[x];
}
inline void dfs2(int x,int fa)
{
	for(register int i=head[x];i;i=e[i].nxt)
	{
		int y=e[i].to;
		if(y==fa)continue;
		up(x,y);
		dfs2(y,x);
	}
}
signed main()
{
	scanf("%lld",&n);
	for(register int i=1;i<n;i++)
	{
		int x,y;
		scanf("%lld%lld",&x,&y);
		add(x,y);
		add(y,x);
	}
	dfs1(1,0);
	dp[1]=f[1];
	dfs2(1,0);
	for(register int i=1;i<=n;i++)
		ans=max(ans,dp[i]);
	printf("%lld",ans);
	return 0;
}

其实也挺水的,只要理解了就好做,跟上面那题差不太多......

其实换根DP本身不难,只是有些题目会特意把它包装得很恶心,所以要注意细节

五.状压DP:

1.定义:

当状态的维度很多,而每一个维度的取值是布尔值时(如dp[2][2]...),则可以用二进制数值去表示一个状态,这种DP称作状压DP

2.基本的位运算:(优先级:四则运算>位运算)

(1) 按位与(\(&\)):两个整数在二进制下逐位比较,同一位有2个1,则结果为1,否则为0

(2) 按位或(\(|\)):两个整数在二进制下逐位比较,同一位有1,则答案为1,否则为0

(3) 按位异或(\(^\)/\(xor\)):两个整数在二进制下逐位比较,同一位上不同,则答案为1,否则为0

(4) 按位取反(~):一个整数在二进制下逐位取反

3.位移操作:

\(x\)<<\(y\):\(x*2^y\)

\(x\)>>\(y\):\(x/2^y\)

4.常用位操作意义:(重点)

典型例题

例1 OpenJ_Bailian4124 海贼王之伟大航路

Code

#include<bits/stdc++.h>
using namespace std;
const int MAXN=17;

int n;
int dp[1<<MAXN][MAXN];
int a[MAXN][MAXN];

int main()
{
	scanf("%d",&n);
	for(register int i=1;i<=n;i++)
	{
		for(register int j=1;j<=n;j++)
		{
			scanf("%d",&a[i][j]);
		}
	}
	memset(dp,0x7f,sizeof(dp));
	dp[1][1]=0;
	for(register int i=1;i<=(1<<n)-1;i++)//枚举所有经过的状态
		for(register int j=1;j<=n;j++)//枚举当前到达的点
		{
			if(!((i>>(j-1))&1))
				continue;//i的第j为0,无效的状态 
			for(register int k=1;k<=n;k++)
			{
				if((i>>(k-1))&1)//第k个点已经经过 
					dp[i][j]=min(dp[i][j],dp[i^(1<<(j-1))][k]+a[k][j]);
			}
		} 
	printf("%d\n",dp[(1<<n)-1][n]);
	return 0;
}

这题作为状压DP的基础题,还是不难理解的,思路在注释中已体现

例二 P1171 售货员的难题

Code

#include<bits/stdc++.h>
using namespace std;
const int MAXN=20;

int dp[1<<MAXN][MAXN],w[MAXN][MAXN];
int n;

int main()
{
	scanf("%d",&n);
	for(register int i=0;i<=n-1;i++)
		for(register int j=0;j<=n-1;j++)
			scanf("%d",&w[i][j]);
	memset(dp,0x3f,sizeof(dp));
	dp[1][0]=0;
	for(register int i=0;i<(1<<n);i++)//枚举所有经过的状态 
		for(register int j=0;j<=n-1;j++)//枚举当前到达的点
		{
			if(!((i>>j)&1))
				continue;//i的第j为0,无效的状态 
			for(register int k=0;k<=n-1;k++)
			{
				if(((i>>k)&1)&&j!=k)//第k个点已经经过 
					dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+w[k][j]);
			}
		} 
	int minn=0x7f7f7f;
	for(register int i=0;i<n;i++)
	{
		minn=min(minn,dp[(1<<n)-1][i]+w[i][0]);
	}
	printf("%d\n",minn);
	return 0;
}

这道题和上一题区别不大,只不过最后要统计终点到各村庄的距离并求min,但是这题很恶心的一点就是,又卡空间又卡常数,所以空间不能开太大,并且时间方面也要做些优化。

总的来说,状压DP并不难理解,难就难在位运算弄不清楚,所以要多熟悉各种位运算的技巧

标签:nxt,head,int,那些,MAXN,DP,dp
来源: https://www.cnblogs.com/yhx-error/p/16220365.html