好题记录树形dp

题目链接:[Problem - D2 - Codeforces](https://codeforces.com/contest/1733/problem/D2)

dfs实现动态规划

题意:给定两个01串,要求经过操作使得第一个串等于第二个串。可以选择两个位置翻转,如果两个位置距离为1,那么翻转的代价为x,距离大于1则代价为y。求最小代价。

 

#include<bits/stdc++.h>
using namespace std;
vector<int>pos;
long long dp[5005][5005];
long long n,x,y;
long long get(long long l,long long r)
{
	if(l+1==r)return min(2*y,x);//如果l,r相邻,我们可以通过两次y操作达到修改相邻的效果,所以我们需比较一下2*y和x的大小,选择最优的
	else return min(y,x*(r-l));//同理,对于l,r不相邻,我们也可以通过x(r-l)次x操作达到修改l和r的效果。
}
long long dfs(long long l,long long r)
{
	if(l>=r)return 0;
	if(dp[l][r]!=-1)return dp[l][r];//剪枝
	long long res=1e18;
	res=min(res,dfs(l+1,r-1)+get(pos[l],pos[r]));
	res=min(res,dfs(l,r-2)+get(pos[r-1],pos[r]));
	res=min(res,dfs(l+2,r)+get(pos[l],pos[l+1]));
	return dp[l][r]=res;
}
void solve()
{
	
	cin>>n>>x>>y;
	string a,b;
	cin>>a>>b;
       for(int i=0;i<=n;i++)
		for(int j=0;j<=n;j++)
		dp[i][j]=-1;
		pos.clear();
    for(int i=0;i<n;i++)if(a[i]!=b[i])pos.push_back(i);
    if(pos.size()%2==1)//不同的个数为奇数个,我们肯定是无法使他们相同的,因为每次都是修改2个
	{
		cout<<-1<<endl;
		return ;
	}
	if(pos.size()==0)
	{
		cout<<0<<endl;
		return ;
	}
	if(pos.size()==2)//两个相邻的情况
	{
		if(pos[0]+1==pos[1])cout<<min(2*y,x)<<endl;
		else cout<<min(y,x*(pos[1]-pos[0]))<<endl;
		return ;
	}
	if(y<=x)
	{
		cout<<pos.size()/2*y<<endl;
		return;
	}
	else
	{
		cout<<dfs(0,pos.size()-1)<<endl;
	}
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	cin>>t;
	while(t--)
	{
		
		solve();
        
	}
   system("pause");
	return 0;
}

思路:对于x>=y的情况比较简单,只需尽量全部选择y即是最优。麻烦的是当x<y的时候我们的贪心策略就失效了。

别人的题解:

容易发现,相距越远的两个数,用x的花费越大,而用y的花费不变;因此如果我们要用y,就会选择最远的两个数用

用dp [ i ] [ j ] 表示解决i~j所需的最小花费,接着我们考虑dp [ i ] [ j ] 从什么地方转移过来

可以发现,每次操作只有三种情况

1.**改变开头的两个位置**,即从dp[ l+2 ] [ r ]转移到dp[ l ] [ r ]

2.**改变结尾的两个位置**,即从dp[ l ] [ r-2 ]转移到dp[ l ] [ r ]

3.**改变开头和结尾各一个位置**,即从dp[ l+1 ] [ r-1 ]转移到dp[ l ] [ r ]

由此,我们得出了状态转移方程,可以直接用记忆化搜索(比较方便)

题解链接https://zhuanlan.zhihu.com/p/566175996       https://zhuanlan.zhihu.com/p/566141853

题目链接:https://www.luogu.com.cn/problem/P2014?contestId=90483

#include<bits/stdc++.h>
using namespace std;
long  long  head[500],ver[3000],nex[3000],w[3000],f[500][500];
long long  cnt=0,n=0,m=0;
void add(int from,int to,int val)
{
	ver[++cnt]=to;
	nex[cnt]=head[from];
	w[cnt]=val;
	head[from]=cnt;
}
void dfs(int now,int fa)
{
	for(int i=head[now];i;i=nex[i])
	{
		int to=ver[i];
		if(to==fa)continue;
		dfs(to,now);//递归到子节点
		for(int j=m;j>=0;j--)
		{
			for(int k=0;k<j;k++)
			{
				f[now][j]=max(f[now][j],f[now][k]+f[to][j-k-1]+w[i]);
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,i,y);
	}
	dfs(0,-1);
	cout<<f[0][m]<<endl;
	return 0;
}

典型的树上背包,每个节点可以有很多的子节点,我们可以先让我们当前要处理的节点与他其中一个节点进行01背包,然后将处理好的当前节点再去和其他节点进行01背包,相当于将当前处理好的节点和已经被01背包过的子节点看作一个整体。

for(int j=m;j>=0;j--)
		{
			for(int k=0;k<j;k++)
			{
				f[now][j]=max(f[now][j],f[now][k]+f[to][j-k-1]+w[i]);
                //f[now][k]是从0到j的,是以前的状态,我们在求f[now][k]要用到,所以外面那个for循环要倒着进行。
			}
		}

在进行01背包的时候,最外面的for循环应倒着循环,这是因为我们再背包的时候要用到以前的状态来推出当前的状态,倒着循环就可以避免因改变f数组的值而对当前需要推的节点造成影响

树形dp:


//	}
	for(int i=head[now];i;i=nex[i])
	{
		int to=ver[i];
		if(to==fa)continue;
		dfs(to,now);
        //某一节点的某一状态等于它的子节点的其余状态另外两种状态相加,然后再与其余节点相乘即可算出答案
	    f[now][1]=(f[now][1]*((f[to][2]+f[to][3])%mod))%mod;
	    f[now][2]=(f[now][2]*((f[to][1]+f[to][3])%mod))%mod;
	    f[now][3]=(f[now][3]*((f[to][2]+f[to][1])%mod))%mod;
	}
}

int main()
{
	cin>>n>>k;
	for(int i=1;i<n;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,y);
		add(y,x);
	}
	
	for(int i=1;i<=k;i++)
	{
		int x,c;
		cin>>x>>c;
		f[x][c]=1;
	}
	for(int i=1;i<=n;i++)
	{
		if(f[i][1]==0&&f[i][2]==0&&f[i][3]==0)
		{
			f[i][1]=1;
			f[i][2]=1;
			f[i][3]=1;
		}
	}
	dfs(1,0);
	cout<<(f[1][1]+f[1][2]+f[1][3])%mod;
	return 0;
}

思路:此题我们需将除已染色的节点的三个状态都赋值成1,已染色的节点染色的状态赋值成一其余的状态赋值成0,

换根dp:

链接题目:https://www.luogu.com.cn/problem/P3047

题意:给你一棵 n个点的树,点带权,对于每个节点求出距离它不超过 k 的所有节点权值和 mi

#include<bits/stdc++.h>
using namespace std;
int n,k,head[100009],ver[100009*2],nex[100009*2],c[100009],f[100009][21],d[100009][21];
int cnt=0;
void add(int from,int to)
{
	ver[++cnt]=to;
	nex[cnt]=head[from];
	head[from]=cnt;
}
void dfs1(int now,int fa)
{
	for(int i=0;i<=k;i++)f[now][i]=c[now];
	for(int i=head[now];i;i=nex[i])
	{
		int to=ver[i];
		if(to==fa)continue;
		dfs1(to,now);
		for(int i=1;i<=k;i++)
		f[now][i]+=f[to][i-1];
	}
}
void dfs2(int now,int fa)
{
//		d[now][1]+=f[fa][0];
//		for(int i=2;i<=k;i++)
//		{
//			d[now][i]+=(d[fa][i-1]-f[now][i-2]);
//		}//由父亲节点把当前的节点的d[now][k]推出,但是不知道为什么错了
       
	for(int i=head[now];i;i=nex[i])
	{
		int to=ver[i];
		if(to==fa)continue;
		
        d[to][1]+=f[now][0];//k=1要单独考虑,因为下面的for循环当k=1数组下标会小于0
        for(int i=2;i<=k;i++)
        {
        	d[to][i]+=d[now][i-1]-f[to][i-2];
		}//有当前节点已经做好,并由当前节点,将其儿子节点全部推出
		dfs2(to,now);
	}
}
int main()
{
	cin>>n>>k;
	for(int i=1;i<n;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,y);
		add(y,x);
	}
	for(int i=1;i<=n;i++)cin>>c[i];
	dfs1(1,-1);
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=k;j++)
		d[i][j]=f[i][j];
	}
	dfs2(1,-1);
	for(int i=1;i<=n;i++)
	{
		cout<<d[i][k]<<endl;
	}
	return 0;
}

  一题比较好象的换根dp。换根dp就是先进行一次dfs将一个节点的答案算出,并由此节点通过与个节点答案的关系,并进行第二次dfs将算出节点的值推出其他还没算出节点的值。此题就是先将一号节点的答案通过第一次dfs算出,然后再通过节点间的关系转移过去就行了,主要是当k=1的时候要注意数组下标会小于0,所以要单独考虑。

题目链接:https://www.luogu.com.cn/problem/CF219D

#include<bits/stdc++.h>
using namespace std;
int head[200009],ver[200009*2],nex[200009*2],w[200009*2];
int cnt=0,f[200009];
void add(int from,int to,int val)
{
	ver[++cnt]=to;
	nex[cnt]=head[from];
	w[cnt]=val;
	head[from]=cnt;
}
void dfs(int now,int fa)
{
	for(int i=head[now];i;i=nex[i])
	{
		
		int to=ver[i];
		if(to==fa)continue;
		dfs(to,now);
		f[now]+=(f[to]+w[i]);
	}
}
void dfs1(int now,int fa)
{
     for(int i=head[now];i;i=nex[i])
     {
     	int to=ver[i];
     	if(to==fa)continue;
     	if(w[i]==1)
     	{
     		f[to]=f[now]-1;
		 }
		 else
		 {
		 	f[to]=f[now]+1;
		 }
     	dfs1(to,now);
	 }
}
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<n;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,y,0);
		add(y,x,1);
	}
	dfs(1,-1);
	dfs1(1,-1);
	int minn=1e9;
	for(int i=1;i<=n;i++)
	{
		if(minn>f[i])minn=f[i];
	}
	cout<<minn<<endl;
	for(int i=1;i<=n;i++)
	{
		if(f[i]==minn)cout<<i<<" ";
	}
	return 0;
}

思路:对于这题,我们在建树的时候可以将指向我的边的权值设为1,由我指向别的节点的边设为0,这用链式前向星很容易实现。然后我们只需找出到个点路程总和最近的节点即可。这题也是用换根来实现的。

  if(w[i]==1)//改变是由子节点指向当前节点的
     	   {
     		f[to]=f[now]-1;//所以该子节点到个点距离和只需由当前节点减一,即将这条边翻转一下即可得到。
		   }
		     else  //改变是由当前节点指向子节点的
		   {
		 	f[to]=f[now]+1;
		   }

一般的换根都是由当前节点推他的子节点,这样不容易出错,由当前节点的父亲节点把当前节点推出,不仅代码不好写,思路也有点乱,以后换根还是由当前节点将他的子节点推出吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值