动态规划基础笔记

背包问题


 

01背包

 

 一般的动态规划要先考虑好状态,这个状态是一个集合,要能分成几个子集,然后从这些子集(小问题),推到这一整个集合(大问题),且求解过程是一样的,就可以可以转换成大问题分解成小问题一个一个求解,最后合并

先要知道状态表示什么

再要知道dp的属性,应该跟所求有关,只会有最大值,最小值,和数量的情况。

然后要知道你这个问题集合里,能从小问题推到大问题这个条件是什么(满足这个条件,才能推过来),还有是从哪些大问题,化解成的小问题

还有状态计算,也就是问题集合的划分,把每一种情况算出来,要求不重,不漏

有一些状态需要拐个弯才能求解(直接求解比较麻烦,所以改变思路但是是等价原来的),比如说含i,可以先求不含i,最后再补上i,是等价的

但是注意,有些集合划分出来的可能是空集,也就是不存在,就必须j<vi的时候,j-vi就不存在了,写代码的时候要注意一下

 

例题

[AcWing] 2. 01背包问题(C++实现)0-1背包问题模板题_c++0-1背包模版-CSDN博客

 二维

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

const int N=1005;
int n, m, v[N], w[N], f[N][N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=0; j<=m; j++)
		{
			f[i][j]=f[i-1][j];
			if (j>=v[i]) f[i][j]=max(f[i][j], f[i-1][j-v[i]]+w[i]);
		}
	
	printf("%d", f[n][m]);
	return 0;
}

  

一维写法(滚动优化)

由于第i件物品只跟第i-1件物品有关,每次都是这件跟前一件有关,所以先去掉一维

 

 

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

const int N=1005;
int n, m, v[N], w[N], f[N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=0; j<=m; j++)
		{
			f[j]=f[j]; //这里是没啥用的,可以删掉
			if (j>=v[i]) f[j]=max(f[j], f[j-v[i]]+w[i]); //删掉上一行之后,我们发现这里j可以直接从v[i]开始遍历
		}
	
	printf("%d", f[m]);
	return 0;
}

  整理一下变成

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

const int N=1005;
int n, m, v[N], w[N], f[N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=v[i]; j<=m; j++)
			f[j]=max(f[j], f[j-v[i]]+w[i]);
		
	
	printf("%d", f[m]);
	return 0;
}

  但是这样是不对的,关键在于j循环的顺序   举例证明的方法:AcWing 2. 01背包问题 - AcWing 

看完举例的方法之后,你就会发现,这句话等价与

f[i][j]=max(f[i][j], f[i][j-v[i]]+w[i]);

但是和原来不符,原因是从小打大循环时,会出现前面值被改的情况,但是从大到小就可以保证前面值不会改变

 

因为i-1的值已经在前面被更新过了,覆盖了
为了避免这个问题,所以要逆序更新,即先更新第i个,然后更新第i-1个,从而保证第i-1个不被覆盖

 

终极写法:

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

const int N=1005;
int n, m, v[N], w[N], f[N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=m; j>=v[i]; j--)
			f[j]=max(f[j], f[j-v[i]]+w[i]);
		
	
	printf("%d", f[m]);
	return 0;
}

  

完全背包

 

 

 因为k=0,也可以用下面的式子推出来f[i-1][j],所以就不分类讨论0,1~k的情况了

 

AcWing 3. 完全背包问题 - AcWing

 

朴素做法

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

const int N=1005;
int n, m, v[N], w[N], f[N][N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=0; j<=m; j++)
			for (int k=0; k*v[i]<=j; k++)
				f[i][j]=max(f[i][j], f[i-1][j-k*v[i]]+k*w[i]);
	
	printf("%d", f[n][m]);
	return 0;
}

  O(nm^2)=O(10^9),很慢

 

优化做法

有了上面的规律,可以化简称下面这样。01背包是从上一层转移过来,完全背包就是从这一层转移过来

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

const int N=1005;
int n, m, v[N], w[N], f[N][N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=0; j<=m; j++)
		{
			f[i][j]=f[i-1][j];
			if (j>=v[i]) f[i][j]=max(f[i][j], f[i][j-v[i]]+w[i]);
		}
	
	printf("%d", f[n][m]);
	return 0;
}

  

再滚动数组优化

终极写法:

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

const int N=1005;
int n, m, v[N], w[N], f[N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=v[i]; j<=m; j++) //这里就不会出现01背包的情况了,毕竟前面选了多少次都可以,后面再选也没事
			f[j]=max(f[j], f[j-v[i]]+w[i]);
	
	printf("%d", f[m]);
	return 0;
}

  

 

多重背包

 

方程也是跟之前差不多

朴素版

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

const int N=105;
int n, m, v[N], w[N], s[N], f[N][N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) scanf("%d%d%d", &v[i], &w[i], &s[i]);
	
	//f[0][0~m]=0
	
	for (int i=1; i<=n; i++)
		for (int j=v[i]; j<=m; j++)
			for (int k=0; k<=s[i] && k*v[i]<=j; k++)
				f[i][j]=max(f[i][j], f[i-1][j-k*v[i]]+k*w[i]);
	
	printf("%d", f[n][m]);
	return 0;
}

  

优化版

类似倍增

我可以选的物品数量是

0,1,2,3,4........s

但是有必要选那么多吗

可以用倍增的思想,假设s=200

1,2,4,8,16,32,64,128

我们发现

0可以凑,选1可以凑0~1,选2可以凑2~3(0+2~1+2),就可以凑0~3。选4可以凑4~7(0+4~3+4),就可以凑0~7,选8,可以凑8~15(0+8~7+8)

选的数                    1,2,4,8

凑出的数的右端点  1,3,7,15

你还可以发现凑到的数的右端点是 2a-1,其中a=2^k,就是2(2^k)-1,就是2^(k+1)-1

但是128(选的数,也就是a)不能要,因为128*2-1=255(2a-1),超过了s,所以要不了

最后2的k次方超过了,就补一个数c

那是否能凑数0~s的所有数呢,是可以的,相当于这些打包的数(选2^k个物品或者c个物品)里面用01背包的方法选一样。

然后再选73,可以凑73~200(0+73~127+73),就可以凑0~200(s)了

怎么求c呢?s-(2^(k+1)-1)

 

一般情况

0,1,2,3,4........s

1,2,4,8,16.....2^k,c

已知c<2^(k+1)

我们能凑出来0~2^(k+1)-1,根据上面得知

这里面的方案,可以凑出  c~2^(k+1)-1+c        (0+c, 2^(k+1)-1+c)

就是凑出 0~2^(k+1)-1+c

因为2^(k+1)-1+c就是s(用求c的式子,把那个式子带入c,就是2^(k+1)-1+s-(2^(k+1)-1)     前后抵消,剩一个s   ),所以可以凑出0~s

 

 

 

 

总结:

 

si拆完,做一遍01背包,原本时间nvs,优化到 nvlogs

 

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

const int N=25000;
int n, m, v1, w1, s1, v[N], w[N], f[N], cnt=0;
int main() 
{
	scanf("%d%d", &n, &m);
	while (n--)
	{
		scanf("%d%d%d", &v1, &w1, &s1);
		int k=1;
		while (k<=s1)
		{
			cnt++;
			v[cnt]=v1*k;
			w[cnt]=w1*k;
			s1-=k;
			k*=2;
		}
		if (s1>0)
		{
			cnt++;
			v[cnt]=v1*s1;
			w[cnt]=w1*s1;
		}
	}
	
	for (int i=1; i<=cnt; i++)
		for (int j=m; j>=v[i]; j--)
			f[j]=max(f[j], f[j-v[i]]+w[i]);
	printf("%d", f[m]);
	return 0;
}

  

 分组背包

 

跟01背包差不多,但是有个k,枚举的是组数

 

 

 

 

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

const int N=105;
int n, m, v[N][N], w[N][N], s[N], f[N];
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++) 
	{
		scanf("%d", &s[i]);
		for (int j=1; j<=s[i]; j++)
			scanf("%d%d", &v[i][j], &w[i][j]);
	}
	
	for (int i=1; i<=n; i++)
		for (int j=m; j>=0; j--)
			for (int k=1; k<=s[i]; k++)
				if (j>=v[i][k]) f[j]=max(f[j], f[j-v[i][k]]+w[i][k]);
	
	printf("%d", f[m]);
	return 0;
}

  

 

 

线性DP


 

像一条线一样,有线性关系。比较好求

数字三角形

AcWing 898. 数字三角形-CSDN博客

 

最长上升子序列

 B3637 最长上升子序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

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

const int N=5005;
int n, a[N], f[N], ans=0;
int main() 
{
	scanf("%d", &n);
	for (int i=1; i<=n; i++) scanf("%d", &a[i]);
	
	for (int i=1; i<=n; i++)
	{
		f[i]=1;
		for (int j=1; j<i; j++)
			if (a[j]<a[i]) f[i]=max(f[i], f[j]+1);
	}
		
	for (int i=1; i<=n; i++) ans=max(ans, f[i]);
	printf("%d", ans);
	return 0;
}

  如果要保留路径呢?存一下就好了(这里输出是倒序)

 

 

 

 

 优化版

AcWing 896. 最长上升子序列 II - AcWing

有贪心的思想

记录一下长度为i结尾的上升子序列的结尾数最小是多少,这样可以保证后面的数尽可能的大,选法多一点

因为长度越小,结尾的数越小,长度越大,结尾的数越大,长度从小到大,数也会从小到大

由于有单调性,所以用二分来找最小的这个数

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

const int N=5005;
int n, a[N], q[N], ans=0, L=0, R=0, res=0;
int main() 
{
	scanf("%d", &n);
	for (int i=1; i<=n; i++) scanf("%d", &a[i]);
	
	q[0]=-2e9;
	for (int i=1; i<=n; i++)
	{
		L=0, R=ans, res=0;
		while (L<=R)
		{
			int mid=L+R>>1;
			if (q[mid]<a[i]) res=mid+1, L=mid+1;
			else R=mid-1;
		}
		ans=max(ans, res);
		q[res]=a[i];
	}
	
	printf("%d", ans);
	return 0;
}

  

 

编辑距离

P2758 编辑距离 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

 

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

const int N=2005;
string a, b;
int n=0, m=0, f[N][N];
int main() 
{
	cin>>a>>b;
	a=' '+a, b=' '+b;
	n=a.size()-1, m=b.size()-1;
	
	for (int i=1; i<=m; i++) f[0][i]=i;
	for (int i=1; i<=n; i++) f[i][0]=i;
	
	for (int i=1; i<=n; i++)
		for (int j=1; j<=m; j++)
			{
				f[i][j]=min(f[i][j-1], f[i-1][j])+1;
				if (a[i]==b[j]) f[i][j]=min(f[i][j], f[i-1][j-1]);
				else f[i][j]=min(f[i][j], f[i-1][j-1]+1);
			}
	
	printf("%d", f[n][m]);
	return 0;
}

  

 

 

 

 

 

 

 

 

 

最长公共子序列

 

 第一种情况可以去掉,因为后两张包含他,后两种情况虽然重复,但是是求最大值,所以没问题,求数量的时候就不能重复了

求最大值时转移方程可以重复

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

const int N=1e3+5;
int n, a[N], b[N], f[N][N], ans=0;
int main() 
{
	scanf("%d", &n);
	for (int i=1; i<=n; i++) scanf("%d", &a[i]);
	for (int i=1; i<=n; i++) scanf("%d", &b[i]);
	
	for (int i=1; i<=n; i++)
		for (int j=1; j<=n; j++)
		{
			//f[i][j]=f[i-1][j-1];
			f[i][j]=max(f[i][j-1], f[i-1][j]);
			if (a[i]==b[j]) f[i][j]=max(f[i][j], f[i-1][j-1]+1);
		}
	
	printf("%d", f[n][n]);
	return 0;
}

  这里是整数写法,但一样的

 

 

区间DP


 

 

P1880 [NOI1995] 石子合并 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

 

 集合划分,是用一条线(k),把区间分成左右两段。因为每一段最少有1的长度,所以是1~k-1

划分过程:

 

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

const int N=105;
int n, a[N], s[N], f[N][N];
int main() 
{
	scanf("%d", &n);
	for (int i=1; i<=n; i++) 
	{
		scanf("%d", &a[i]);
		s[i]=s[i-1]+a[i];
	}
	
	for (int len=2; len<=n; len++)
		for (int i=1; i+len-1<=n; i++)
		{
			int L=i, R=i+len-1;
			f[L][R]=1e9;
			for (int k=L; k<R; k++)
			{
				f[L][R]=min(f[L][R], f[L][k]+f[k+1][R]+s[R]-s[L-1]);
			}
		}
			
	
	printf("%d", f[1][n]);
	return 0;
}

  

 

计数类DP

考虑DP时,属性改成数量即可

AcWing 900 整数划分_整数划分acwing-CSDN博客

 

 

 

方法1

 由于是有序的,可以考虑背包,这就是一个完全背包(总重量为n,物品重量为i,记f是选1~i个物品,重量恰好是j)

 转移方程:

 转换

 转换

 

 

其中f[0]=1,就是1个物品不选,重量为0,有1种,默认情况,可以理解为去掉二维之前,是f[0][0]=1

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

const int N=1005, MOD=1e9+7;
int n, f[N];
int main() 
{
	scanf("%d", &n);
	
	f[0]=1;
	for (int i=1; i<=n; i++)
		for (int j=i; j<=n; j++)
			f[j]=(f[j]+f[j-i])%MOD;
	
	printf("%d", f[n]);
	return 0;
}

  

 

 

 

方法2

 状态计算,划分是用所有方法是否有一个数是1来划分的

f[i][j]表示总和为i,划分为了j个数的方案数。状态划分可标识为这j个数中是否包含了1;如果包含1,则f[i][j] = f[i-1][j-1],即方案数等于去掉一个1后的方案数,j - 1个数总和是i - 1;如果这j个数中不包含1,则f[i][j] = f[i - j][j],即方案数等于将j个数每个都减去1后构成总和为i - j的方案数。因此可以得到状态转移方程为f[i][j] = f[i-1][j-1] + f[i-j][j]。最后的解为f[n][1] + ... + f[n][n],即所有能构成总和是n的划分方案的和。

0个数的和就是0,所以f[0][0]=1,是一种情况

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

const int N=1005, MOD=1e9+7;
int n, f[N][N], ans=0;
int main() 
{
	scanf("%d", &n);
	
	f[0][0]=1;
	for (int i=1; i<=n; i++)
		for (int j=1; j<=i; j++)
			f[i][j]=(f[i-1][j-1]+f[i-j][j])%MOD;
	
	for (int i=1; i<=n; i++) ans=(ans+f[n][i])%MOD;
	printf("%d", ans);
	return 0;
}

  

 

数位统计DP

尤其注意分情况讨论!!!

给定两个整数 a 和 b,求 a 和 b 之间的所有数字中0~9的出现次数。

例如,a=1024,b=1032,则 a 和 b 之间共有9个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中‘0’出现10次,‘1’出现10次,‘2’出现7次,‘3’出现3次等等…

输入格式
输入包含多组测试数据。

每组测试数据占一行,包含两个整数 a 和 b。

当读入一行为0 0时,表示输入终止,且该行不作处理。

输出格式
每组数据输出一个结果,每个结果占一行。

每个结果包含十个用空格隔开的数字,第一个数字表示‘0’出现的次数,第二个数字表示‘1’出现的次数,以此类推。

数据范围
0<a,b<100000000
输入样例:

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

 

输出样例

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247

 

思路:

类似前缀和,现在我们只用想一下如何实现count函数 ,拿x=1举例,知道怎么求x=1的情况,其他情况都可以求出来

假设这个数n=abcdefg,我们假设要求第4位出现的次数(其他位置同理可求),那就把整个数分成两段,然后再分情况讨论这两段

但是必须满足一个条件,1 <= xxx1yyy <= abcdefg

可以分情况讨论前半段xxx,如果

  • xxx<abc,那么yyy随便取,因为整个数已经<=abcdefg了(000~999都行),可得知这种情况方案数是  abc*1000(记得别漏了000的情况!)
  • xxx=abc,这里要分情况讨论d
    •   d<1,不可能,不满足条件
    •   d=1,yyy只能取到efg,加上0,方案数是,efg+1
    •   d>1,yyy随便取,0~999,方案数是1000种
  •  xxx>abc,不可能的情况,不满足条件

 

还有一些细节,比如枚举1在第一位的时候,第一种情况是不存在的。

如果求的x=0,那么000的情况不存在,因为不可能存在前导零,所以000不合法,从100开始没有前导零

注意,为什么x=1为什么前面不从100开始呢?不是也有前道导吗?因为就是相当于枚举个位、然后再枚举十位、最后枚举的百位

 

 

 

 但是要注意一下x=0的情况,(2)的所有情况其实都没问题,因为没有前导零,但是(1)的情况可能会有,所以要注意一下(1)

所以不能有前导零,那么就从001开始取,就ok了

 

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

int a, b;
int get(vector<int> a, int L, int R)
{
	int res=0;
	for (int i=L; i<=R; i++)
		res=res*10+a[i];
	return res;
}
int count(int n, int x)
{
	if (!n && !x) return 1; //注意一下边界 n=0,x=0其实答案是1
	if (!n) return 0; //n=1,x!=0,答案就只能是0了
	
	int res=0;
	vector<int> a;
	while (n>0)
	{
		a.push_back(n%10);
		n/=10;
	}
	reverse(a.begin(), a.end());
	n=a.size();
	//把n拆成每一位
for (int i=0; i<n; i++) { if (i!=0) { res+=get(a, 0, i-1)*pow(10, n-1-i); if (!x) res-=pow(10, i); //x=0的情况 } if (x==a[i]) res+=get(a, i+1, n-1)+1; //记得+1 if (x<a[i]) res+=pow(10, n-1-i); //cout<<res<<endl; } return res; } int main() { while (true) { scanf("%d%d", &a, &b); if (a==0 && b==0) break; if (a>b) swap(a, b); for (int i=0; i<10; i++) printf("%d ", count(b, i)-count(a-1, i)); puts(""); } return 0; }

  

 

状态压缩


 状态是一个整数,但要看成二进制数,每一位0/1表示不同情况

 

蒙德里安的梦想

输入样例

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例

1
0
1
2
3
5
144
51205

  观察发现,我们只需要确定横着放的数量,就是答案了,因为横着放完,竖着放的方法是唯一的

 状态表示是竖着的一列(i),,状态是j(0/1,1表示上一列伸出来了一个小方格,0表示没伸出来),的方案数量

右下角是两个样例

初始值是在0列,且没有向后面伸出,所以合法,是一种方案数

答案就是第m列,且没有向后面伸出

 

 

 状态转移是由前一列合法状态,转移k到这一列可以和j匹配(j表示的是这一列的状态,k表示前一列的状态)的和

具体分析一下哪些状态合法?

1.没有冲突,比如这两个黑色方框冲突了,就不合法,也就是 j&k==0

 2.有连续个奇数个0,比如这里,i列的j是01001,第i-1列k,10010,那会发现竖着的方框摆不了

就是j | k 不存在连续奇数个0,这里可以预处理求出

 

大概是4*1e7的复杂度

f[0][0]=1是为什么?0列由-1列转移过来,但是根本没有-1列,所以这-1列压根不用延伸一个方框到0列来,因为本身就没有-1列,所以不用延伸方块,这也是一种方案,所以f[0][0]=1

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

const int N=12, M=1<<N;
int n, m;
long long f[N][M];
bool st[M];
int main()
{
	while (cin>>n>>m, n || m)
	{
		memset(f, 0, sizeof f);
		
		for (int i=0; i<(1<<n); i++)
		{
			st[i]=true;
			int cnt=0;
			
			for (int j=0; j<n; j++)
				if ((i>>j) & 1)
				{
					if (cnt & 1) 
					{
						st[i]=false;
						break;
					}
					cnt=0;
				}
				else cnt++;
			
			if (cnt & 1) st[i]=false;
		}
		
		f[0][0]=1;
		for (int i=1; i<=m; i++)
			for (int j=0; j<(1<<n); j++)
				for (int k=0; k<(1<<n); k++)
					if ((j&k)==0 && st[j | k]) 
						f[i][j]+=f[i-1][k];
		
		printf("%lld\n", f[m][0]);
	}
	return 0;
} 

  

 

 

最短Hamilton路径

输入

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出

18

  

 

 走过点是i,这里要用状态压缩,表示i

可以枚举走到i的前一个点

从0号点走到0号点,距离是0,走了1个点,最后停留在0,所以f[1][0]=0

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

const int N=21, M=1<<N;
int n, mp[N][N], f[M][N];
int main()
{
	scanf("%d", &n);
	for (int i=0; i<n; i++)
		for (int j=0; j<n; j++)
			scanf("%d", &mp[i][j]);
	
	memset(f, 63, sizeof f);
	f[1][0]=0;
	
	for (int i=0; i<(1<<n); i++) //i表示所有的情况
		for (int j=0; j<n; j++) //j表示走到哪一个点
			if ((i>>j)&1) //第j个点就是第j位,而且i必须包含j,才是有意义的
				for (int k=0; k<n; k++) //k表示走到j这个点之前,以k为终点
					if (((i-(1<<j))>>k) & 1)
						f[i][j]=min(f[i][j], f[i-(1<<j)][k]+mp[k][j]);
	
	printf("%d", f[(1<<n)-1][n-1]);		
	
	return 0;
} 

  

 

树形DP


 

 

 

 

 

 

 

P1352 没有上司的舞会 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

f[x][1]表示选这个点,f[x][0]表示不选这个点

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

const int N=6005;
int n, happy[N], u1, v1, root=0, f[N][2]; //这里一定要开大一点,不能开成1,不然会答案错误,卡了2天在这里!
vector<int> a[N];
void dfs(int now, int fa)
{
	f[now][1]=happy[now];
	f[now][0]=0;
	for (int i=0; i<a[now].size(); i++)
	{
		int son=a[now][i];
		
		if (son==fa) continue;
		dfs(son, now);
		
		f[now][1]+=f[son][0];
		f[now][0]+=max(f[son][1], f[son][0]);
	}
}
int main()
{
	scanf("%d", &n);
	for (int i=1; i<=n; i++) scanf("%d", &happy[i]);
	
	root=n*(n+1)/2;
	for (int i=1; i<n; i++)
	{
		scanf("%d%d", &u1, &v1);
		a[u1].push_back(v1);
		a[v1].push_back(u1);
		root-=u1;
	}
	dfs(root, -1);
	
	
	printf("%d", max(f[root][0], f[root][1])); 
	return 0;
} 

  

 

记忆化搜索


 

P1434 [SHOI2002] 滑雪 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

特点:好理解,但是时间比循环版本的长,有可能爆栈

基本套路:

如果dp这个值不是初始值,就修改,否则就返回这个值

 

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

const int N=105;
int n, m, h[N][N], ans=1, f[N][N];
int dx[]={-1, 0, 0, 1}, dy[]={0, -1, 1, 0};
int dp(int x, int y)
{
	if (f[x][y]!=-1) return f[x][y];
	
	f[x][y]=1;
	for (int i=0; i<4; i++)
	{
		int nx=x+dx[i], ny=y+dy[i];
		if (nx>=1 && nx<=n && ny>=1 && ny<=m && h[nx][ny]<h[x][y])
			f[x][y]=max(f[x][y], dp(nx, ny)+1); 
	}
	
	return f[x][y];
}
int main() 
{
	scanf("%d%d", &n, &m);
	for (int i=1; i<=n; i++)
		for (int j=1; j<=m; j++)
			scanf("%d", &h[i][j]);
	
	memset(f, -1, sizeof f);
	for (int i=1; i<=n; i++)
		for (int j=1; j<=m; j++)
			ans=max(ans, dp(i, j));
		
	printf("%d", ans);
	return 0;
}

  

 

 

 

 

  

 

posted @ 2024-02-15 12:49  cn是大帅哥886  阅读(33)  评论(0)    收藏  举报