算法基础课 模板题笔记:动态规划

AcWing算法基础课 动态规划(Dynamic Programming, DP)模板题笔记

分析流程:

  • 状态表示 f ( i , j , ⋯   ) f(i,j,\cdots) f(i,j,)
    • 集合:(满足某些条件的集合的某种属性、选法)
    • 属性:(最大值、最小值、元素数量)
  • 状态计算:集合划分(一般原则:不重不漏) → \rightarrow 状态转移方程
  • 时间复杂度计算:状态数量 × 转移的计算量

常用算法代码模板 (1) :基础算法
常用算法代码模板 (2) :数据结构
常用算法代码模板 (3) :搜索与图论
常用算法代码模板 (4) :数学知识
算法选择——由数据范围反推算法时间复杂度



1 背包问题

N N N 个物品和容量为 V V V 的背包,第 i i i 个物品的体积为 v i v_i vi 、价值为 w i w_i wi​​ ,问求在背包装得下的情况下所能挑出物品的总价值。

01背包问题

特点:每件物品只有一个,最多装一次

AcWing 2. 01背包问题

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:从前 i i i 个物品中选总体积不超过 j j j 的所有选法的价值的最大值
  • 属性:最大值

f ( i , j ) f(i,j) f(i,j) 表示的选法分为两大类——不含第 i i i 个: f ( i − 1 , j ) f(i-1,j) f(i1,j) ;包含第 i i i 个: f ( i − 1 , j − v i ) + w i f(i-1,j-v_i)+w_i f(i1,jvi)+wi 。状态转移方程为
f ( i , j ) = max ⁡ ( f ( i − 1 , j ) ,   f ( i − 1 , j − v i ) + w i ) ,   v i ≤ j f(i,j)=\max(f(i-1,j),\ f(i-1,j-v_i)+w_i),\ v_i≤j f(i,j)=max(f(i1,j), f(i1,jvi)+wi), vij

/* 暴力DP */
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]);
	}

观察代码可知01背包的当前状态( i i i )仅与前一状态( i − 1 i-1 i1 )有关,故可使用滚动数组,将空间缩小到一维 f ( j ) f(j) f(j) 。改写时需注意:转移时直接使用上一层 i − 1 i-1 i1)的体积时应从大到小枚举,防止覆盖;使用本层( i i i)的体积则从小到大枚举!

本题应当将 j j j 改为从大到小枚举,优化后的完整题解如下。

时间复杂度: O ( n 2 ) O(n^2) O(n2)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);

	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\n", f[n][m]);

	return 0;
}

完全背包问题

特点:每件物品有无限个,可无限装

AcWing 3. 完全背包问题

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:从前 i i i 个物品中选总体积不超过 j j j 的所有选法
  • 属性:价值的最大值

f ( i , j ) f(i,j) f(i,j) 表示的选法分成若干类,主要为两大类——不选择物品 i i i f ( i − 1 , j ) f(i-1,j) f(i1,j);物品 i i i k   ( k > 0 ) k\ (k>0) k (k>0) 个: f ( i − 1 , j − k v i ) + k w i f(i-1,j-kv_i)+kw_i f(i1,jkvi)+kwi 。合并这两大类情况后的状态转移方程为
f ( i , j ) = max ⁡ ( f ( i − 1 , j − k v i ) + k w i ) ,   k ≥ 0 ,   k v i ≤ j f(i,j)=\max(f(i-1,j-kv_i)+kw_i),\ k≥0,\ kv_i≤j f(i,j)=max(f(i1,jkvi)+kwi), k0, kvij

时间复杂度: O ( n 3 ) O(n^3) O(n3)

/* 暴力DP*/
for (int i = 1; i <= n; i++)
	for (int j = 0; j <= m; j++) 
		for (int k = 0; k * v[i] <= j; k++)	// 合并了不选的情况(k=0)
			f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);

优化比较次数:由状态转移方程可得 f ( i , j ) = max ⁡ ( f ( i − 1 , j ) ,   f ( i − 1 , j − v i ) + w i ,   f ( i − 1 , j − 2 v i ) + 2 w i ,   ⋯   ) ⇒ f ( i , j − v i ) = max ⁡ ( f ( i − 1 , j − v i ) ,   f ( i − 1 , j − 2 v i ) + w i ,   ⋯   ) f(i,j)=\max(f(i-1,j),\ f(i-1,j-v_i)+w_i,\ f(i-1,j-2v_i)+2w_i,\ \cdots) \Rightarrow \\f(i,j-v_i)=\max(f(i-1,j-v_i),\ f(i-1,j-2v_i)+w_i,\ \cdots) f(i,j)=max(f(i1,j), f(i1,jvi)+wi, f(i1,j2vi)+2wi, )f(i,jvi)=max(f(i1,jvi), f(i1,j2vi)+wi, ) 观察易得可用 f ( i , j − v i ) + w i f(i,j-v_i)+w_i f(i,jvi)+wi 直接替换原方程右边最值函数中 k > 0 k>0 k>0 时的所有比较项。由此将比较时间消耗从 O ( n ) O(n) O(n) 大幅优化至 O ( 1 ) O(1) O(1),原状态转移方程可化简为
f ( i , j ) = max ⁡ ( f ( i − 1 , j ) ,   f ( i , j − v i ) + w i ) ,   v i ≤ j f(i,j)=\max(f(i-1,j),\ f(i,j-v_i)+w_i),\ v_i≤j f(i,j)=max(f(i1,j), f(i,jvi)+wi), vij

时间复杂度: O ( n 2 ) O(n^2) O(n2)

/* 优化比较次数后的DP */
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])	// 只需比较1次
				f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
	}

参照01背包问题,可继续优化至一维 f ( j ) f(j) f(j) j j j 应保持从小到大枚举(因为继续使用本层的体积/物品 i i i 可能会用到多次)。

最终优化后的完整题解如下。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);
	
	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;
}

多重背包问题

特点:每个物品 i i i s i s_i si

(1) 朴素算法

AcWing 4. 多重背包问题

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:从前 i i i 个物品中选总体积不超过 j j j 的所有选法
  • 属性:价值的最大值

同完全背包问题,可将 f ( i , j ) f(i,j) f(i,j) 表示的选法分成若干类:分别为选 0 , 1 , 2 , ⋯   , s i 0,1,2,\cdots,s_i 0,1,2,,si 个。状态转移方程为
f ( i , j ) = max ⁡ ( f ( i − 1 , j − k v i ) + k w i ) ,   0 ≤ k ≤ s i ,   k v i ≤ j f(i,j)=\max(f(i-1,j-kv_i)+kw_i),\ 0≤k≤s_i,\ kv_i≤j f(i,j)=max(f(i1,jkvi)+kwi), 0ksi, kvij

时间复杂度: O ( n 3 ) O(n^3) O(n3)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int 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]);
	
	/* 暴力DP */
    for (int i = 1; i <= n; i++)
        for (int j = 0; 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\n", f[n][m]);

    return 0;
}

(2) 二进制优化

AcWing 5. 多重背包问题 II

多重背包中每个物品为有限项,无法像完全背包一样直接替换优化。需使用二进制优化方法——将物品按组打包:对于物品 i i i 的个数 s i s_i si ,将其分解为 2 0 , 2 1 , 2 2 , ⋯   , 2 k , c 2^0,2^1,2^2,\cdots,2^k,c 20,21,22,,2k,c ,其中 k = ⌊ log ⁡ s i ⌋ ,   c = s i − ( 2 0 + 2 1 + ⋯ + 2 k ) k=\lfloor \log s_i\rfloor,\ c=s_i-(2^0+2^1+\cdots+2^k) k=logsi, c=si(20+21++2k)。因为用 2 0 , 2 1 , 2 2 , ⋯   , 2 k 2^0,2^1,2^2,\cdots,2^k 20,21,22,,2k 可拼出 [ 0 ,   2 k + 1 − 1 ] [0,\ 2^{k+1}-1] [0, 2k+11] 上所有整数,又 c = s i − ( 2 k + 1 − 1 ) < 2 k + 1 c=s_i-(2^{k+1}-1)<2^{k+1} c=si(2k+11)<2k+1 ,所以用 2 0 , 2 1 , 2 2 , ⋯   , 2 k , c 2^0,2^1,2^2,\cdots,2^k,c 20,21,22,,2k,c 可拼出 [ 0 ,   s i ] [0,\ s_i] [0, si] 上所有整数。

由此枚举部分的时间消耗从 O ( n ) O(n) O(n) 降至 O ( log ⁡ n ) O(\log n) O(logn) 。同时将问题转化为01背包问题(每一组有且只有选或不选两种方案),可以再进一步优化成一维求解。

时间复杂度: O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 12010, M = 2010;	// N = 1000 * log2(1000)

int n, m;
int v[N], w[N];
int f[M];	// 降为一维,只需开1e3量级的长度

int main() {
	scanf("%d%d", &n, &m);
	
	int cnt = 0;	// 记录打包分组后的物品的个数
	for (int i = 1; i <= n; i++) {
		int tv, tw, s;
		scanf("%d%d%d", &tv, &tw, &s);
		int k = 1;	// 2的cnt次幂
		while (k <= s) {	// 每k个为一组
			cnt++;
			v[cnt] = tv * k;
			w[cnt] = tw * k;
			s -= k;
			k *= 2;
		}
		if (s > 0) {	// 剩余的单独一组
			cnt++;
			v[cnt] = tv * s;
			w[cnt] = tw * s;
		}
	}
	
	/* 二进制优化后,将多重背包转化成01背包 */
	n = cnt;	// 更新为打包后物品的数量(每种物品只有1个)
	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\n", f[m]);
	
	return 0;
}

分组背包问题

特点:所有物品分若干组,每组里只能选一个

AcWing 9. 分组背包问题

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:从前 i i i 物品中选总体积不超过 j j j 的所有选法
  • 属性:价值的最大值

可将 f ( i , j ) f(i,j) f(i,j) 表示的选法分成若干类——第 i i i 组不选: f ( i − 1 , j ) f(i-1,j) f(i1,j);第 i i i 组的 s i s_i si 个物品中选第 k   ( k > 0 ) k\ (k>0) k (k>0) 个物品: f ( i − 1 , j − v i k ) + w i k f(i-1,j-v_{ik})+w_{ik} f(i1,jvik)+wik 。状态转移方程为 f ( i , j ) = max ⁡ ( f ( i − 1 , j ) ,   f ( i − 1 , j − v i k ) + w i k ) ,   0 < k ≤ s i ,   v i k ≤ j f(i,j)=\max(f(i-1,j),\ f(i-1,j-v_{ik})+w_{ik}),\ 0<k≤s_i,\ v_{ik}≤j f(i,j)=max(f(i1,j), f(i1,jvik)+wik), 0<ksi, vikj同样可以直接去掉 i i i 所在维度优化成一维。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &s[i]);
		for (int j = 0; 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 = 0; k < s[i]; k++)	// 枚举该组中的物品
				if (v[i][k] <= j)			// 同01背包最初解法操作
					f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
	
	printf("%d\n", f[m]);
	
	return 0;
}

2 线性DP

状态转移方程存在明显线性关系。

例1:数字三角形

AcWing 898. 数字三角形

在这里插入图片描述

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:所有从起点走到点 ( i , j ) (i,j) (i,j) 的路径
    • 规定从起点出发沿三角形左、右两条边做 x x x 轴、 y y y 轴,起点为 ( 1 , 1 ) (1,1) (1,1),点 ( i , j ) (i,j) (i,j) 上的数为 a i j a_{ij} aij
  • 属性:路径长度的最大值

f ( i , j ) f(i,j) f(i,j) 表示的集合可分为两类——来自左上 f ( i − 1 , j − 1 ) + a i j f(i-1,j-1)+a_{ij} f(i1,j1)+aij 、来自右上 f ( i − 1 , j ) + a i j f(i-1,j)+a_{ij} f(i1,j)+aij 。状态转移方程为
f ( i , j ) = max ⁡ ( f ( i − 1 , j − 1 ) + a i j ,   f ( i − 1 , j ) + a i j ) f(i,j)=\max(f(i-1,j-1)+a_{ij},\ f(i-1,j)+a_{ij}) f(i,j)=max(f(i1,j1)+aij, f(i1,j)+aij)

时间复杂度: O ( n 2 ) O(n^2) O(n2)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n;
int a[N][N];
int f[N][N];

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= i; j++)
			scanf("%d", &a[i][j]);
	
	for (int i = 0; i <= n; i++)	// 设置边界值:将三角形及其左右两边外赋负无穷
		for (int j = 0; j <= n + 1; j++)
			f[i][j] = -INF;
	
	f[1][1] = a[1][1];	// 初始化起点
	
	for (int i = 2; i <= n; i++)
		for (int j = 1; j <= i; j++)
			f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
	
	int res = -INF;
	for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
	printf("%d\n", res);
	
	return 0;
}

例2:最长上升子序列

(1) 朴素算法

AcWing 895. 最长上升子序列

状态表示 f ( i ) f(i) f(i)

  • 集合:所有以第 i i i 个数 a i a_i ai 结尾的上升子序列
  • 属性:上升子序列长度的最大值

f ( i ) f(i) f(i) 表示的集合可分为若干类(不一定都存在)——上升子序列中只有 a i a_i ai 一个数: 1 1 1倒数第二个数 a j   ( 1 ≤ j ≤ i − 1 ) a_j\ (1≤j≤i-1) aj (1ji1) f ( j ) + 1 f(j)+1 f(j)+1(若存在 a j < a i a_j<a_i aj<ai )。状态转移方程为 f ( i , j ) = { 1 , a j ≥ a i max ⁡ ( f ( j ) + 1 ) , a j < a i ,   0 ≤ j ≤ i − 1 f(i,j)=\begin{cases} 1, & a_j≥a_i \\ \max(f(j)+1), & a_j<a_i\\ \end{cases},\ 0≤j≤i-1 f(i,j)={1,max(f(j)+1),ajaiaj<ai, 0ji1

时间复杂度: O ( n 2 ) O(n^2) O(n2)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int a[N];
int f[N];

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;	// 只有a[i]一个数
		for (int j = 1; j < i; j++) {
			if (a[j] < a[i])
				f[i] = max(f[i], f[j] + 1);
		}
	}
	
	int res = 0;
	for (int i = 1; i <= n; i++) res = max(res, f[i]);
	printf("%d\n", res);
	
	return 0;
}

(2) 贪心+二分优化

AcWing 896. 最长上升子序列 II

贪心优化

思路:如果以 a j a_j aj 结尾和以 a k a_k ak 结尾的上升子序列长度相同,那么就取 a j a_j aj a k a_k ak 之中较小的那一个的子序列,小的数更有可能构成更长的上升子序列

因此可以采用贪心思想,开一个变长数组q[],长度为len,其中q[i]存储长度为 i i i 的上升子序列中结尾数最小的子序列的结尾数

对于 a i a_i ai ,在q[]中找到子序列长度/数组下标 k k k 尽可能大的数 q k q_k qk,满足 q k < a i q_k<a_i qk<ai 。若查找失败,则q[]尾插 a i a_i ai ;若查找成功则覆盖该数(无论如何 a i a_i ai 都应放至下标 k k k 上)。最终数组的长度len即为答案。

时间复杂度: O ( n 2 ) O(n^2) O(n2)

for(int i = 0; i < n; i ++){
     int k = 0;
     while(k < len && q[k] < a[i]) k ++;	// 顺序查找子序列长度最大的小于a[i]的数
     q[k] = a[i];	// a[i]无论如何都应位于位置k上
     if (k == len) len ++;	// 当k为len时说明查找越界,不存在小于a[i]的数,a[i]尾插
 }

二分优化
在这里插入图片描述
画图易证q[]中元素单调递增,则可用二分来优化查找过程,查找最大小于当前数的数(查找右边界),由上图可知 a i a_i ai 应放在该数的后一位上。最终优化后的完整题解如下。

时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int n;
int a[N];
int q[N], len;	// q[i]存储长度为i的上升子序列中结尾数最小的子序列的结尾数

int main() {
	scanf("%d", &n);
	for (int i = 0; i < n; i++) scanf("%d", &a[i]);
	
	for (int i = 0; i < n; i++) {
		int l = 0, r = len;	// 套用二分模板,二分查找最大的小于a[i]的数q[r]
		while (l < r) {
			int mid = l + r + 1 >> 1;
			if (q[mid] < a[i]) l = mid;
			else r = mid - 1;
		}
		q[r + 1] = a[i];	// 画图可知,a[i]应放在r + 1位置上
		if (r + 1 > len) len++;	// 查找失败则相当于直接让a[i]尾插
	}
	
	printf("%d\n", len);
	
	return 0;
}

例3:最长公共子序列

AcWing 897. 最长公共子序列

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:所有在第1个序列 { a n } \{a_n\} {an} 的前 i i i 个字母出现,且在第2个序列 { b m } \{b_m\} {bm} 的前 j j j 个字母中出现的子序列
  • 属性:长度的最大值

根据字母 a i ,   b j a_i,\ b_j ai, bj 的选择情况(分别用2位二进制数的每一位来表示, 0 0 0 : 不选, 1 1 1 : 选)来划分 f ( i , j ) f(i,j) f(i,j) 表示的集合—— 00 ,   01 ,   10 ,   11 00,\ 01,\ 10,\ 11 00, 01, 10, 11 ,其中易得 00 00 00 f ( i − 1 , j − 1 ) f(i-1,j-1) f(i1,j1) 11 11 11 f ( i − 1 , j − 1 ) + 1 f(i-1,j-1)+1 f(i1,j1)+1

尽管 01 01 01 情况无法简单给出准确的状态表示,与 f ( i − 1 , j ) f(i-1,j) f(i1,j) 所表示的集合不完全等同,但由上述集合定义可知 f ( i − 1 , j ) f(i-1,j) f(i1,j) 包含 01 01 01 情况,因此可用其替代。同理, f ( i , j − 1 ) f(i,j-1) f(i,j1) 包含 10 10 10 ,也可作为替代。根据 max ⁡ ( A , B , C ) = max ⁡ ( max ⁡ ( A , B ) , max ⁡ ( B , C ) ) \max(A,B,C)=\max(\max(A,B),\max(B,C)) max(A,B,C)=max(max(A,B),max(B,C))(其中 B B B 的重复出现不影响最终最值结果)可知,尽管该种集合划分方式显然存在重复问题,但由于所求的是最值重复划分不会影响最终结果,注意如果所求的是数量则不得重复划分。

观察可知, 00 00 00 情况也包含于 f ( i − 1 , j ) f(i-1,j) f(i1,j) f ( i , j − 1 ) f(i,j-1) f(i,j1) 之中,因此实际只有3种情况: f ( i − 1 , j ) ,   f ( i , j − 1 ) ,   f ( i − 1 , j − 1 ) + 1 f(i-1,j),\ f(i,j-1),\ f(i-1,j-1)+1 f(i1,j), f(i,j1), f(i1,j1)+1 。 状态转移方程为
f ( i , j ) = { f ( i − 1 , j − 1 ) + 1 , a i = b j max ⁡ ( f ( i − 1 , j ) ,   f ( i , j − 1 ) ) , a i ≠ b j f(i,j)=\begin{cases} f(i-1,j-1)+1, & a_i=b_j\\ \max(f(i-1,j),\ f(i,j-1)), & a_i≠b_j \end{cases} f(i,j)={f(i1,j1)+1,max(f(i1,j), f(i,j1)),ai=bjai=bj

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main() {
	scanf("%d%d", &n, &m);
	scanf("%s%s", a + 1, b + 1);	// 从下标1开始存字符
	
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++) {
			f[i][j] = max(f[i - 1][j], f[i][j - 1]);
			if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
		}
	
	printf("%d\n", f[n][m]);
	
	return 0;
}

例4:最短编辑距离

AcWing 902. 最短编辑距离

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:将a[1 ... i]转化成b[1 ... j]所做的修改方式
  • 属性:修改次数的最小值

要使得a[1 ... i]b[1 ... j]匹配,需对a[i]进行题中所述3种之一操作,由此反推出a[i]b[j]前的字符应满足的前提条件。根据上述思路将 f ( i , j ) f(i,j) f(i,j) 表示的集合分成三类——

  1. 删除:把a[i]删除使得匹配,需先满足a[1 ... i-1]b[1 ... j]匹配,集合即为 f ( i − 1 , j ) + 1 f(i-1,j)+1 f(i1,j)+1
  2. 插入:在a[i]之后插入使得匹配,即执行a[i+1]=b[j],需先满足a[1 ... i]b[1 ... j-1]匹配,集合即为 f ( i , j − 1 ) + 1 f(i, j-1)+1 f(i,j1)+1
  3. 替换:将a[i]直接替换成b[j],需先满足a[1 ... i-1]b[1 ... j-1]匹配。当a[i] != b[i]时,集合即为 f ( i − 1 , j − 1 ) + 1 f(i-1,j-1)+1 f(i1,j1)+1;若已有a[i] == b[j],则不应执行操作,集合为 f ( i − 1 , j − 1 ) f(i-1,j-1) f(i1,j1)

因此状态转移方程为 f ( i , j ) = { min ⁡ ( f ( i − 1 , j ) + 1 , f ( i , j − 1 ) + 1 , f ( i − 1 , j − 1 ) ) , a i = b j min ⁡ ( f ( i − 1 , j ) + 1 , f ( i , j − 1 ) + 1 , f ( i − 1 , j − 1 ) + 1 ) , a i ≠ b j f(i,j)=\begin{cases} \min(f(i-1,j)+1,f(i,j-1)+1,f(i-1,j-1)), & a_i=b_j\\ \min(f(i-1,j)+1,f(i,j-1)+1,f(i-1,j-1)+1), & a_i≠b_j \end{cases} f(i,j)={min(f(i1,j)+1,f(i,j1)+1,f(i1,j1)),min(f(i1,j)+1,f(i,j1)+1,f(i1,j1)+1),ai=bjai=bj

边界处理:当其中一方为空串时,修改次数即为另一方非空串的长度,则 f ( i , 0 ) = i ,   f ( 0 , j ) = j f(i,0)=i,\ f(0,j)=j f(i,0)=i, f(0,j)=j

时间复杂度: O ( n 2 ) O(n^2) O(n2)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main() {
	scanf("%d%s%d%s", &n, a + 1, &m, b + 1);
	
	for (int i = 0; i <= n; i++) f[i][0] = i;
	for (int i = 0; i <= m; i++) f[0][i] = i;
	
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++) {
			f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 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\n", f[n][m]);
	
	return 0;
}

变式:AcWing 899. 编辑距离 核心DP方法完全一致,考察字符串的存储与处理。


3 区间DP

例:石子合并

AcWing 282. 石子合并

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:所有将下标区间 [ i ,   j ] [i,\ j] [i, j] 上的石子合并成一堆石子的合并方式
  • 属性:合并方式代价的最小值

分治区间 [ i ,   j ] [i,\ j] [i, j] 为左右子区间,根据左子区间的划分情况将 f ( i , j ) f(i,j) f(i,j) 表示的集合按如下方式分成若干类—— 1 , 2 , 3 , ⋯   , j − i − 1 , j − i 1,2,3,\cdots,j-i-1,j-i 1,2,3,,ji1,ji 。设划分给左子区间的右端点为 k k k ,则将 [ i ,   k ] , [ k + 1 ,   j ] [i,\ k],[k+1,\ j] [i, k],[k+1, j] 合并后的代价的最小值为两侧子区间上的最小值之和再加上本区间上所有石子的质量(此为合并子区间必须进行的不可分解的最小操作),即 f ( i , k ) + f ( k + 1 , j ) + s j − s i − 1 f(i,k)+f(k+1,j)+s_j-s_{i-1} f(i,k)+f(k+1,j)+sjsi1 ,其中 s i s_i si 表示第 i i i 个石子的质量对应的前缀和。状态转移方程为
f ( i , j ) = min ⁡ ( f ( i , k ) + f ( k + 1 , j ) + s j − s i − 1 ) ,   1 ≤ k ≤ j − i f(i,j)=\min(f(i,k)+f(k+1,j)+s_j-s_{i-1}),\ 1≤k≤j-i f(i,j)=min(f(i,k)+f(k+1,j)+sjsi1), 1kji

时间复杂度: O ( n 3 ) O(n^3) O(n3)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 310;

int n;
int s[N];
int f[N][N];

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		int x;
		scanf("%d", &x);
		s[i] = s[i - 1] + x;	// 求前缀和
	}
	
	for (int len = 2; len <= n; len++)	// 枚举区间[i, j]的长度(可跳过1,此时子区间长度为0,值为0)
		for (int i = 1; i + len - 1 <= n; i++) {	// 枚举左端点i
			int j = i + len - 1;	// 对应的右端点j
			f[i][j] = 0x3f3f3f3f;	// 求最小值,需先初始化为无穷
			for (int k = i; k < j; k++)	// 枚举分界点k
				f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
		}
	
	printf("%d\n", f[1][n]);
	
	return 0;
}

4 计数类DP

例:整数划分

AcWing 900. 整数划分

(1) 总和不变,个数变

实为要求恰好装满背包的完全背包问题。

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:从前 i i i 个数中选总和恰好 j j j 的所有选法
  • 属性:选法数量

直接参照完全背包问题转移的推导与优化过程即可,可以视作权值等同下标 v i = i v_i=i vi=i ,则无需取最值,状态转移方程可化简为 f ( i , j ) = f ( i − 1 , j ) + f ( i , j − i ) f(i,j)=f(i-1,j)+f(i,j-i) f(i,j)=f(i1,j)+f(i,ji) 同样可以继续降为一维 f ( j ) f(j) f(j)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010, MOD = 1e9 + 7;

int n;
int f[N];

int main() {
	scanf("%d", &n);
	
	f[0] = 1;	// 和为0的选法为1(不装)

	for (int i = 1; i <= n; i++)
		for (int j = i; j <= n; j++)	// 这里物品i的权即为i,故j直接从i开始遍历
			f[j] = (f[j] + f[j - i]) % MOD;
	
	printf("%d\n", f[n]);
	
	return 0;
}

(2) 个数不变,总和变

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:总和为 i i i恰好表示成 j j j 个数的和的所有选法
  • 属性:选法数量

根据所选的这 j j j 个数里的最小值 f ( i , j ) f(i,j) f(i,j) 表示的集合划分为两部分——最小值为 1 1 1 :当前所有方案均去掉这个“ 1 1 1”,即得 f ( i − 1 , j − 1 ) f(i-1,j-1) f(i1,j1) ;最小值 > 1 >1 >1 :这 j j j 个数各自减 1 1 1 后仍为正整数,即得 f ( i − j , j ) f(i-j,j) f(ij,j)。状态转移方程为
f ( i , j ) = f ( i − 1 , j − 1 ) + f ( i − j , j ) f(i,j)=f(i-1,j-1)+f(i-j,j) f(i,j)=f(i1,j1)+f(ij,j) 因此划分 n n n 的方案数为 f ( n , 1 ) + f ( n , 2 ) + ⋯ + f ( n , n ) f(n,1)+f(n,2)+\cdots+f(n,n) f(n,1)+f(n,2)++f(n,n)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010, MOD = 1e9 + 7;

int n;
int f[N][N];

int main() {
	scanf("%d", &n);
	
	f[0][0] = 1;	// 和为0、选0个的选法为1种
	
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= i; j++)	// 这里j从1枚举到i
			f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % MOD;
	
	int res = 0;
	for (int i = 1; i <= n; i++) res = (res + f[n][i]) % MOD;
	printf("%d\n", res);
	
	return 0;
}

5 数位统计DP

重点在逻辑上的分类讨论。

例:计数问题

AcWing 338. 计数问题

使用前缀和的思想,将求区间上的解转化成求前缀的解:定义函数 count ( n , x ) \text{count}(n, x) count(n,x) 表示 [ 1 ,   n ] [1,\ n] [1, n] 上的数各数位中数字 x x x 出现的次数。根据前缀和的思想,原题的解即为 count ( b , x ) − count ( a − 1 , x ) \text{count}(b, x)-\text{count}(a-1, x) count(b,x)count(a1,x) ,因此将原题转化为子问题:求 [ 1 ,   n ] [1,\ n] [1, n] 上的数各数位中数字 x x x 出现的次数。设 n = a 1 a 2 ⋯ a m ‾ n=\overline{a_1a_2\cdots a_m} n=a1a2am ,其中 m m m n n n 的位数。求 x x x 在第 i i i 位上出现的次数 C i ( n , x ) C_i(n,x) Ci(n,x) 的过程如下:

1 ≤ b 1 b 2 ⋯ b i − 1 x b i + 1 ⋯ b m ‾ ≤ a 1 a 2 ⋯ a i − 1 a i a i + 1 ⋯ a m ‾ = n 1≤\overline{b_1b_2\cdots b_{i-1}xb_{i+1}\cdots b_m}≤\overline{a_1a_2\cdots a_{i-1}a_ia_{i+1}\cdots a_m}=n 1b1b2bi1xbi+1bma1a2ai1aiai+1am=n

  1. b 1 b 2 ⋯ b i − 1 ‾ ∈ [ 0 ,   a 1 a 2 ⋯ a i − 1 ‾ − 1 ] \overline{b_1b_2\cdots b_{i-1}}\in[0,\ \overline{a_1a_2\cdots a_{i-1}}-1] b1b2bi1[0, a1a2ai11] ,则   b i + 1 b i + 2 ⋯ b m ‾ \ \overline{b_{i+1}b_{i+2}\cdots b_m}  bi+1bi+2bm任取 m − i m-i mi 位数字
    1. x > 0 x>0 x>0 ,则正常计算, 总方案数为 a 1 a 2 ⋯ a i − 1 ‾ × 1 0 m − i \overline{a_1a_2\cdots a_{i-1}}\times 10^{m-i} a1a2ai1×10mi
    2. x = 0 x=0 x=0 ,则合法 b 1 b 2 ⋯ b i − 1 ‾ ∈ [ 1 ,   a 1 a 2 ⋯ a i − 1 ‾ − 1 ] \overline{b_1b_2\cdots b_{i-1}}\in[1,\ \overline{a_1a_2\cdots a_{i-1}}-1] b1b2bi1[1, a1a2ai11] ,总方案数为 ( a 1 a 2 ⋯ a i − 1 ‾ − 1 ) × 1 0 m − i (\overline{a_1a_2\cdots a_{i-1}}-1)\times 10^{m-i} (a1a2ai11)×10mi
  2. b 1 b 2 ⋯ b i − 1 ‾ = a 1 a 2 ⋯ a i − 1 ‾ \overline{b_1b_2\cdots b_{i-1}} = \overline{a_1a_2\cdots a_{i-1}} b1b2bi1=a1a2ai1 ,则
    1. a i < x a_i<x ai<x,此时   a 1 a 2 ⋯ a i − 1 x b i + 1 ⋯ b m ‾ > a 1 a 2 ⋯ a i − 1 x 0 a i + 1 ⋯ a m ‾ \ \overline{a_1a_2\cdots a_{i-1}xb_{i+1} \cdots b_m}>\overline{a_1a_2\cdots a_{i-1}x_0a_{i+1} \cdots a_m}  a1a2ai1xbi+1bm>a1a2ai1x0ai+1am ,其中 b i + 1 b i + 2 ⋯ b m ‾ \overline{b_{i+1}b_{i+2}\cdots b_m} bi+1bi+2bm 可任取,不存在方案
    2. a i = x a_i=x ai=x,则 b i + 1 b i + 2 ⋯ b m ‾ ∈ [ 0 ,   a i + 1 a i + 2 ⋯ a m ‾ ] \overline{b_{i+1}b_{i+2}\cdots b_m}\in[0,\ \overline{a_{i+1}a_{i+2}\cdots a_m}] bi+1bi+2bm[0, ai+1ai+2am] ,方案数为 a i + 1 a i + 2 ⋯ a m ‾ + 1 \overline{a_{i+1}a_{i+2}\cdots a_m}+1 ai+1ai+2am+1
    3. a i > x a_i>x ai>x,则 b i + 1 b i + 2 ⋯ b m ‾ \overline{b_{i+1}b_{i+2}\cdots b_m} bi+1bi+2bm任取 m − i m-i mi 位数字,方案数为 1 0 m − i 10^{m-i} 10mi

注意 x = 0 x=0 x=0 的情况:

  1. i i i 位数不得全为 0 0 0 ,否则 x x x 成为非法的前导零,因此 b 1 b 2 ⋯ b i − 1 ‾ \overline{b_1b_2\cdots b_{i-1}} b1b2bi1 的下限为 00 ⋯ 01 ‾ \overline{00\cdots 01} 0001,比 x ≠ 0 x≠0 x=0 时少1种方案。
  2. 无需计算 C 1 ( n , 0 ) C_1(n,0) C1(n,0),因为 0 0 0 不可能位于数字首位。

综上, x x x n n n 的第 i i i 位上出现的次数 C i ( n , x ) = { ( a 1 a 2 ⋯ a i − 1 ‾ − c ) × 1 0 m − i , a i < x ( a 1 a 2 ⋯ a i − 1 ‾ − c ) × 1 0 m − i + a i + 1 a i + 2 ⋯ a m ‾ + 1 , a i = x ( a 1 a 2 ⋯ a i − 1 ‾ − c ) × 1 0 m − i + 1 0 m − i , a i > x ,   c = { 0 , x ≠ 0 1 , x = 0 C_i(n,x)=\begin{cases} (\overline{a_1a_2\cdots a_{i-1}}-c)\times 10^{m-i}, & a_i<x \\ (\overline{a_1a_2\cdots a_{i-1}}-c)\times 10^{m-i}+\overline{a_{i+1}a_{i+2}\cdots a_m}+1, & a_i=x \\ (\overline{a_1a_2\cdots a_{i-1}}-c)\times 10^{m-i}+10^{m-i}, & a_i>x \end{cases},\ c=\begin{cases} 0, & x≠0\\ 1, & x=0 \end{cases} Ci(n,x)= (a1a2ai1c)×10mi,(a1a2ai1c)×10mi+ai+1ai+2am+1,(a1a2ai1c)×10mi+10mi,ai<xai=xai>x, c={0,1,x=0x=0 则最终答案 count ( n , x ) = C 1 ( n , x ) + C 2 ( n , x ) + ⋯ + C m ( n , x ) \text{count}(n,x)=C_1(n,x)+C_2(n,x)+\cdots+C_m(n,x) count(n,x)=C1(n,x)+C2(n,x)++Cm(n,x)

#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;

/* 获取数组num中下标l至r位置上的数字构成的数 */
int get(vector<int> &num, int l, int r) {
	int res = 0;
	for (int i = l; i >= r; i--) res = res * 10 + num[i];	// 注意num为倒着存数
	return res;
}

/* 返回n中出现数字x的次数 */
int count(int n, int x) {
	if (n == 0) return 0;	// 参与计数的应为正整数,n=0时返回0
	
	vector<int> num;
	while (n) {
		num.push_back(n % 10);
		n /= 10;
	}
	
	int res = 0;
	for (int i = num.size() - 1; i >= 0; i--) {	// 从高位开始枚举每一位,这里i = len - idx
		if (i == num.size() - 1 && x == 0) continue;	// x=0时不判首位(因为必不可能有前导0)
		
		if (i < num.size() - 1) {	// 首位无需处理此部分
			if (x) res += get(num, num.size() - 1, i + 1) * pow(10, i);
			else res += (get(num, num.size() - 1, i + 1) - 1) * pow(10, i);
		}
		
		if (num[i] == x) res += get(num, i - 1, 0) + 1;
		else if (num[i] > x) res += pow(10, i);
	}
	
	return res;
}

int main() {
	int a, b;
	while (scanf("%d%d", &a, &b), a || b) {
		if (a > b) swap(a, b);
		
		for (int i = 0; i <= 9; i++)
			printf("%d ", count(b, i) - count(a - 1, i));
		puts("");
	}
	
	return 0;
}

6 状态压缩DP

用二进制数保存状态:二进制数的每一位表示一个物品, 0 / 1 0/1 0/1 表示不同的状态。

例1:蒙德里安的梦想

AcWing 291. 蒙德里安的梦想

摆完横向小方格之后纵向小方格的位置是唯一确定的,因此只需考虑按照列摆放横向小方格。可以用 N N N 的二进制数( [ 0 ,   2 N − 1 ] [0,\ 2^N-1] [0, 2N1] 上的所有整数)来存储全部 N N N 行的每一行状态。

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:当前摆放第 i i i 列,从第 i − 1 i-1 i1出至该列的横着的方块状态为 j j j 的全部方案。
    • j j j 为表示第 i i i 列每一行位置的状态的二进制数—— 从高位至低位,每一位对应该列从上至下每一行的状态,其中 0 0 0:无方块, 1 1 1 :有方块。
  • 属性:方案的数量

对于 f ( i , j ) f(i,j) f(i,j) ,设第 i − 1 i-1 i1 列的状态表示数为 k k k ,则可进行状态转移的条件为——

  1. 两列不得冲突:j & k == 0为真。
  2. i − 1 i-1 i1 列连续的空白格子( k k k 中连续的 0 0 0)必须是偶数(以腾出位置给纵向小方块)——j | k不存在连续奇数个 0 0 0

满足上述2个条件可进行转移操作—— f ( i , j ) ← f ( i , j ) + f ( i − 1 , k ) f(i,j)\leftarrow f(i,j)+f(i-1,k) f(i,j)f(i,j)+f(i1,k)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 12, M = 1 << N;	// M = 2^N

int n, m;
long long f[N][M];
bool st[M];

int main() {
	while (scanf("%d%d", &n, &m), n || m) {
		memset(f, 0, sizeof f);
		
		for (int i = 0; i < 1 << n; i++) {	// 预处理判断每一列的状态i是否不存在连续奇数个0
			bool success = true;	// 标记是否不存在连续奇数个0;若检测出有连续奇数(cnt&1==1)个0则置false
			int cnt = 0 ;			// 记录当前连续0的个数
			for (int j = 0; j < n; j++)	// 遍历第j行,0 <= j <= n-1
				if (i >> j & 1) {		// i右移j位+与1作按位与运算=检测第i列第j位/行:若遇见1,检测当前连续0的个数是否为奇数
					if (cnt & 1) success = false;
					cnt = 0;	// cnt清空(其实无影响,不清空亦可)
				} else cnt++;
			
			if (cnt & 1) success = false;
			st[i] = success;
		}
		
		
		f[0][0] = 1;	// 第1列的前列(记为第0列)必然不可能出现伸出方块至第1列,故仅能摆纵向小方块(第二维=0),设为1
		for (int i = 1; i <= m; i++)	// 从第1列开始摆
			for (int j = 0; j < 1 << n; j++)		// 枚举第i列的所有状态
				for (int k = 0; k < 1 << n; k++)	// 枚举第i-1列的所有状态
					if ((j & k) == 0 && st[j | k])	// 状态转移条件:两列不冲突 且 不存在连续奇数个0("&"优先级低于"==")
						f[i][j] += f[i - 1][k];
		
		printf("%lld\n", f[m][0]);
	}
	
	return 0;
}

例2:最短Hamilton路径

AcWing 91. 最短Hamilton路径

压缩存储路径。

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:目前所有从点 0 0 0 走到点 j j j ,走过的所有点是 i i i 的所有路径
    • i i i 存储路径状态的二进制数—— 从低位到高位,第 k k k 位表示第 k k k 个点是否经过,其中 0 0 0:没经过; 1 1 1 :经过了。
  • 属性:路径长度的最小值

参考线性DP中“倒数第二个点”的分析方法,将 f ( i , j ) f(i,j) f(i,j) 表示的集合进行划分——设倒数第二个点为 k   ( 0 ≤ k < n − 1 ) k\ (0≤k<n-1) k (0k<n1) ,由题可知边 ( k , j ) (k,j) (k,j) 的权为 a [ k , j ] a[k,j] a[k,j] ,从 0 0 0 走到 k k k 的路径即为路径 i i i 除去点 j j j 后的路径 i ′ i' i 。因此状态转移方程即为 f ( i , j ) = min ⁡ ( f ( i ′ , k ) + a [ k ,   j ] ) ,   0 ≤ k ≤ n − 1 f(i,j)=\min(f(i',k)+a[k,\ j]),\ 0≤k≤n-1 f(i,j)=min(f(i,k)+a[k, j]), 0kn1

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 20, M = 1 << N;	// M = 2 ^ N

int n;
int w[N][N];
int f[M][N];

int main() {
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
			scanf("%d", &w[i][j]);
	
	memset(f, 0x3f, sizeof f);	// 初始化为无穷
	f[1][0] = 0;	// 从点0出发到点0路径长度为0(1即意为只有第1个点有经过,pos=idx+1)
	
	for (int i = 0; i < 1 << n; i++)	// 枚举所有路径i(可优化:从1开始枚举,步长为2,因为循环时路径一定从点0开始)
		for (int j = 0; j < n; j++)		// 枚举所有终点j
			if (i >> j & 1)	// i右移j位+与1作按位与运算=检测路径i是否经过点j(显然终点必须在路径上)
				for (int k = 0; k < n; k++)	// 枚举倒数第二个点k
					if (i >> k & 1)	//同上,检测路径i是否经过点k
						f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);	// i-(1<<j):消去i上对应点j的位置上的1
				
	printf("%d\n", f[(1 << n) - 1][n - 1]);	// 1<<n-1使得上溢,正好为走遍所有点的状态表示
	
	return 0;
}

7 树形DP

例:没有上司的舞会

AcWing 285. 没有上司的舞会

状态表示 f ( u , 0 ) ,   f ( u , 1 ) f(u,0),\ f(u,1) f(u,0), f(u,1)

  • 集合:所有以点 u u u 为根的子树中选择,且[ 0 0 0: 不选][ 1 1 1: 选]点 u u u 的方案
  • 属性:快乐值总和的最大值

递归求解:对于 f ( u , 0 ) f(u,0) f(u,0) (选了点 u u u ,故初值为其快乐值 H u H_u Hu),加其俩儿子 s 1 , s 2 , ⋯ s_1,s_2,\cdots s1,s2, 的各状态最大值之和: f ( s 1 , 1 ) , f ( s 2 , 0 ) , ⋯ f(s_1,1),f(s_2,0),\cdots f(s1,1),f(s2,0), f ( s 1 , 0 ) , f ( s 2 , 1 ) , ⋯ f(s_1,0),f(s_2,1),\cdots f(s1,0),f(s2,1), 等,;对于 f ( u , 1 ) f(u,1) f(u,1) ,则不应再选其儿子,直接加上其儿子们的 f ( s i , 0 ) f(s_i,0) f(si,0) 之和即可。由此可得状态转移方程 f ( u , 0 ) = H u + ∑ i max ⁡ ( f ( s i , 0 ) , f ( s i , 1 ) ) f ( u , 1 ) = ∑ i f ( s i , 0 ) f(u,0)=H_u+\sum_i \max(f(s_i,0),f(s_i,1))\\ f(u,1)=\sum_i f(s_i,0) f(u,0)=Hu+imax(f(si,0),f(si,1))f(u,1)=if(si,0)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;	// 邻接表存树
int happy[N];
int f[N][2];
bool has_father[N];

void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u) {
	f[u][1] = happy[u];
	
	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];
		dfs(v);
		
		f[u][1] += f[v][0];
		f[u][0] += max(f[v][0],f[v][1]);
	}
}

int main() {
	memset(h, -1, sizeof h);
	
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &happy[i]);
	
	for (int i = 0; i < n - 1; i++) {	// 树的边数 = 点数 - 1
		int a, b;
		scanf("%d%d", &b, &a);
		add(a, b);
		has_father[b] = true;
	}
	
	int root = 1;
	while (has_father[root]) root++;	// 根结点无父亲
	
	dfs(root);
	
	printf("%d\n", max(f[root][0], f[root][1]));	// 输出选或不选根的结果的较大值
	
	return 0;
}

8 记忆化搜索

例:滑雪

AcWing 901. 滑雪

状态表示 f ( i , j ) f(i,j) f(i,j)

  • 集合:所有从点 ( i , j ) (i,j) (i,j) 开始滑的路径
  • 属性:路径长度的最大值

参考搜索算法技巧,根据上、下、左、右将 f ( i , j ) f(i,j) f(i,j) 表示的集合分为4类。易知任何 f ( i , j ) f(i,j) f(i,j) 的初值均为 1 1 1 (路径仅含起点一个点),设下一步的合法(不出界、满足题设条件)坐标为 ( i ′ , j ′ ) (i',j') (i,j),则状态转移方程为 f ( i , j ) ← max ⁡ ( f ( i , j ) , f ( i ′ , j ′ ) + 1 ) f(i,j)\leftarrow\max(f(i,j),f(i',j')+1) f(i,j)max(f(i,j),f(i,j)+1)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 310;

int n, m;
int h[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};	// 偏移量

int dfs(int x, int y) {
	int &v = f[x][y];
	if (v != -1) return v;
	
	v = 1;	// 最小值为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])
			v = max(v, dfs(nx, ny) + 1);
	}
	return v;
}

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);
	
	int res = 0;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			res = max(res, dfs(i, j));
	
	printf("%d\n", res);
	
	return 0;
}
  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Akira37

💰unneeded

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值