动态规划(Dynamic programming)讲解(线性 DP 篇)

文章目录

动态规划(Dynamic Programing)

动态规划就是通过记住过去所算过的值,来防止重复计算所带来的弊端。这就是为什么动态规划也可以写成记忆化搜索的形式。

下面,我们通过 DP 的题目,假设我们对其的感悟!(大家可以边看边做一下这几道题,都是很好的题目)


第一关:线性DP

第一战: C F 191 A .   D y n a s t y P u z z l e s \color{7F25DF}{CF191A.\space Dynasty\enspace Puzzles} CF191A. DynastyPuzzles

题目描述
难度: ☆☆☆ \color{blue}{☆☆☆} ☆☆☆
题目大意
题目描述

有一个王朝,他们国王的名字用姓氏的简写来标记每一代。为了保证王朝的稳定,现在这个王朝的继承人的名字需要满足继承者名字的第一个字母要和前代名字最后一个字母相同。然后拼接起来的名字,第一个字母和最后一个字母相同。现在有一个考古博士,知道了这个王朝国王和亲戚的名字。问你这个王朝所能够得到的最长字符串。

输入

第一行一个整数 n n n ( 1 ≤ n ≤ 5 ⋅ 1 0 5 1≤n≤5·10^5 1n5105),接下来 n n n行,每行一个非空字符串,全由小写字母组成,字符串长度不超过 10 10 10

输出

最长满足要求的长度,如果没有输出 0 0 0


思路

动态规划莫过于 4 4 4 步:

  1. 状态的定义
  2. 转移的推导
  3. 边界的确定
  4. 结果的输出
状态的定义:

可以设 f i , j f_{i, j} fi,j 表示当前名字的第一个字母为 i i i,最后一个字母为 j j j 的最长串。
但是,字母不容易开数组,所以我们可以将其压缩成 0 ∼ 26 0\sim26 026 的整数。


转移的推导

之后,我们不难想到对于每一个长度为 n n n 字符串 S S S f j , S n = m a x ( f j , S n , f j , S 1 + n ) f_{j, S_n}=max(f_{j, S_n}, f_{j, S_1} + n) fj,Sn=max(fj,Sn,fj,S1+n)
注意,如果这是我们所拼接的第一个串,那么 f S 1 , S n = n f_{S_1, S_n} = n fS1,Sn=n,不能在通过上面的转移方式转移,因为第一个字母只能是 S 1 S_1 S1


边界的确定

因为,我们要确定每一个串是否是我们拼接的第一个串,所以我们上来要把 f f f 数组全部赋值为 i n t int int 的最小值。


结果的输出

那么这道题最终他的名字其实就是首位字母相同的串,故所有串中首位相同的最长串即为答案!
r e s = m a x ( f i , i ) res=max(f_{i, i}) res=max(fi,i)

一首小诗作为总结:

名字首 i i i 最后 j j j,状态设为 f i , j f_{i, j} fi,j
方程转移枚举头,加入 i i i 串转移透。
边界一定要注意,设小为保第一串。
最后结果首尾同,即为最值 f i , i f_{i,i} fi,i


时间复杂度: O ( n ) \color{blue}{O(n)} O(n)
代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 30;

int n;
string name;
int f[N][N];

signed main()
{
	cin >> n;
	
	memset(f, -0x3f, sizeof f);
	for (int i = 1; i <= n; i ++)
	{
		cin >> name;
		int len = name.size(), l = name[0] - 'a', r = name.back() - 'a';
		for (int j = 0; j < 26; j ++)
			f[j][r] = max(f[j][r], f[j][l] + len);
		f[l][r] = max(f[l][r], len); //初始所有值都是-0x3f3f3f3f,所以取个max就能够让第一个串赋值
	}
	
	int res = 0;
	for (int k = 0; k < 26; k ++)
		res = max(res, f[k][k]);
		
	cout << res << endl;
	
	return 0;
}

恭喜您,成功战胜了 C F 191 A .   D y n a s t y P u z z l e s \color{7F25DF}{CF191A.\space Dynasty\enspace Puzzles} CF191A. DynastyPuzzles,奖励怪兽等级一份:

B r o n z e M o n s t e r \color{97EF6A}{Bronze\enspace Monster} BronzeMonster
U n u s u a l M o n s t e r \color{F9E55D}{Unusual\enspace Monster} UnusualMonster
E p i c M o n s t e r \color{7F25DF}{Epic\enspace Monster} EpicMonster
L e g e n d a r y M o n s t e r \color{CC1D26}{Legendary\enspace Monster} LegendaryMonster


第二战: C F 455 A . B o r e d o m \color{F9E55D}{CF455A. \enspace Boredom} CF455A.Boredom

题目描述
难度: ☆☆ \color{blue}{☆☆} ☆☆
题目大意

给定一个有 n n n 个元素的序列 { a n } \{a_n\} {an}。你可以做若干次操作。在一次操作中我们可以取出一个数(假设他为 x x x)并删除它,同时删除所有的序列中值为 x + 1 x+1 x+1 x − 1 x-1 x1 的数。这一步操作会给玩家加上 x x x 分。

输入

输入格式 第一行一个整数 n ( 1 ≤ n ≤ 1 0 5 ) n(1\le n\le 10^5) n(1n105),说明这个序列有多少数。 第二行 n n n 个整数,分别表示 a 1 , a 2 , ⋯   , a n a_1,a_2,\cdots,a_n a1,a2,,an

输出

一个整数,表示玩家最多能获得多少分


思路

动态规划 4 4 4 步:

  1. 状态的定义
  2. 转移的推导
  3. 边界的确定
  4. 结果的输出
状态的定义:

首先我们会想到设 f i , 0 / 1 f_{i, 0/1} fi,0/1 表示 a i a_i ai删除或者是不删除,但是发现不是很好转移,于是我们可以在这里插入图片描述

嘿嘿,才怪。故而,我们再想一想,他前后删除的是所有值,所以设 f i , 0 / 1 f_{i, 0/1} fi,0/1 表示前 i i i 个数中,数字 i i i 删除还是不删除会不会简单一些呢?
在这里插入图片描述


转移的推导

不难想到,设 c n t i cnt_i cnti i i i 出现的次数,
{ f i , 1 = m a x ( f i − 2 , 0 , f i − 2 , 1 ) + c n t i × i f i , 0 = m a x ( f i − 1 , 1 , f i − 1 , 0 ) \begin{cases} & f_{i, 1} = max(f_{i - 2, 0}, f_{i - 2, 1})+cnt_i\times i\\ & f_{i, 0} = max(f_{i - 1, 1}, f_{i - 1, 0}) \end{cases} {fi,1=max(fi2,0,fi2,1)+cnti×ifi,0=max(fi1,1,fi1,0)
因为,如果选了数 i i i,那么贡献就是 c n t i × i cnt_i\times i cnti×i,那么 i − 1 i - 1 i1 也一定不能选。但是 i − 2 i - 2 i2 是可以选的呀!如果不选数 i i i,那么就考虑 i − 1 i - 1 i1选不选。


边界的确定

貌似不需要


结果的输出

不难想,就是 m a x ( f m x , 0 , f m x , 1 ) max(f_{mx, 0}, f_{mx, 1}) max(fmx,0,fmx,1) m x mx mx表示序列中最大的元素。
因为,我们考虑了前面所有的元素,最终就是这两个取其一!

一首小诗总结一下

这题状态要小心,不设下标却设数。
转移若选找前 2 2 2,如果不选看前值。
边界确定仔细想,貌似根本不需要。
最终结果找 m x mx mx,选或不选取个 m a x max max


时间复杂度: O ( n ) \color{blue}{O(n)} O(n)
代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 1e5 + 10;

int n;
int a[N], f[N][2], cnt[N];

signed main()
{
	cin >> n;
	
	int mx = 0;
	for (int i = 1; i <= n; i ++)
		cin >> a[i], cnt[a[i]] ++, mx = max(mx, a[i]);
		
	for (int i = 1; i <= mx; i ++)
	{
		f[i][1] = max(f[i - 2][1], f[i - 2][0]) + cnt[i] * i;
		f[i][0] = max(f[i - 1][1], f[i - 1][0]);
	}
		
	cout << max(f[mx][1], f[mx][0]) << endl;
	
	return 0;
}

恭喜您,成功战胜了 C F 455 A . B o r e d o m \color{F9E55D}{CF455A. \enspace Boredom} CF455A.Boredom,奖励继续看题(嘿嘿)


第三战: C F 1061 C . M u l t i p l i c i t y \color{7F25DF}{CF1061C. \enspace Multiplicity} CF1061C.Multiplicity

题目描述
难度: ☆☆☆ \color{blue}{☆☆☆} ☆☆☆
题目大意

从序列 { a 1 ,   a 2 ,   . .   ,   a n } \{a_1,\ a_2,\ ..\ ,\ a_n\} {a1, a2, .. , an} 中选出非空子序列 { b 1 ,   b 2 ,   . .   ,   b k } \{b_1,\ b_2,\ ..\ ,\ b_k\} {b1, b2, .. , bk},一个子序列合法需要满足 ∀   i ∈ [ 1 ,   k ] ,   i   ∣   b i \forall\ i \in [1,\ k],\ i\ |\ b_i  i[1, k], i  bi。求有多少互不相等的合法子序列,答案对 1 0 9 + 7 10^9 + 7 109+7 取模。

序列 { 1 ,   1 } \{1,\ 1\} {1, 1} 2 2 2 种选法得到子序列 { 1 } \{1\} {1},但 1 1 1 的来源不同,认为这两个子序列不相等。

思路

动态规划需要 5 5 5 步了:

  1. 状态的定义
  2. 转移的推导
  3. 边界的确定
  4. 结果的输出
  5. 动归的优化
状态的定义:

由于题意中设计到了第 i i i 个数必须被 i i i整除,所以我们可以想到长度,于是可以考虑用 f i , j f_{i, j} fi,j 表示对于前 i i i 个数来说,长度为 j j j 的方案数。


转移的推导

{ f i , j = f i , j + f i − 1 , j j ∣ a i f i , j = f i − 1 , j o t h e r w i s e \begin{cases} & f_{i, j} = f_{i, j} + f_{i - 1, j} &j\mid a_i\\ & f_{i, j} = f_{i-1,j} &otherwise \end{cases} {fi,j=fi,j+fi1,jfi,j=fi1,jjaiotherwise
因为,如果 a i a_i ai j j j 的倍数,那么就可以加入序列中,所以可以加上前一个长度。反之,不可以,不能加入,所以长度不减 1 1 1


边界的确定

起初,长度为 0 0 0 时规定有一种转移方式,即 f 0 , 0 = 1 f_{0, 0}=1 f0,0=1


结果的输出

最终的结果就是所有可能长度之和,即 r e s = ∑ i = 1 n f n , i res=\sum\limits_{i=1}^{n}f_{n, i} res=i=1nfn,i


动归的优化

这道题涉及到了动态规划的优化,因为我们会发现,两维的状态根本开不出来,会
所以,这时候,细心地我们发现转移的时候只用到了 i − 1 i-1 i1,那么我们就可以仿照01背包的方式,用个滚动数组把第一维滚掉,当然这样在枚举长度的时候要倒着枚举!
OK,数组是开下了,可是时间呢?还是炸,我们不妨算一下,#^&%!$# ……*&%¥&@,时间复杂度 O ( n 2 ) O(n^2) O(n2)!完了,芭比球了!

没事,不要慌,慌你就输了。我们再细心的观察一下,就会发现,咦好像只有在 j ∣ a i j\mid a_i jai的时候才可以转移,所以我们就能够先把 a i a_i ai 的约数列出来( O ( n ) O(\sqrt{n}) O(n )),然后进行转移( O ( n ) O\sqrt(n) O( n))。

诶?好像可以了诶,时间复杂度到了 O ( n n ) O(n\sqrt{n}) O(nn ),这样就可以跑过去了!


一首小诗总结一下

状态无妨设两维,之后再用滚动压。
转移只当 j ∣ a j\mid a ja,所以可以找约数。
边界 0 0 0 长设为 1 1 1,之后才可好转移。
最后求和所有长,直接输出 A C AC AC 现。


时间复杂度: O ( n n ) \color{blue}{O(n\sqrt{n})} O(nn )
代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 1e6 + 10, mod = 1e9 + 7;

int n;
int a[N];
int f[N];
vector<int> gt;

signed main()
{
	cin >> n;
	
	f[0] = 1;
	for (int i = 1; i <= n; i ++)
	{
		cin >> a[i];
		gt.clear();
		for (int j = 1; j <= a[i] / j; j ++)
			if (a[i] % j == 0)
			{
				gt.push_back(j);
				if (a[i] / j != j) gt.push_back(a[i] / j);
			}
		
		sort(gt.begin(), gt.end(), greater<int>());
		for (auto c : gt)
			f[c] = (f[c] + f[c - 1]) % mod;
	}
	
	int res = 0;
	for (int i = 1; i <= n; i ++)
		res = (res + f[i]) % mod;
		
	cout << res << endl;
	
	return 0;
}

恭喜您,成功战胜了 C F 1061 C . M u l t i p l i c i t y \color{7F25DF}{CF1061C. \enspace Multiplicity} CF1061C.Multiplicity,奖励一次问而必答的机会,验证码6AZ70好像没什么用 )+简单题讲解。


第四战: A B C 311 E − D e f e c t   f r e e   S q u a r e s \color{F9E55D}{ABC311E - Defect\ free\ Squares} ABC311EDefect free Squares

题目描述
题目大意

给出一个 H × W H\times W H×W 的矩阵,每个位置要么有洞,要么没洞,问有多少个每个元素均为正整数的三元组 ( x , y , n ) (x,y,n) (x,y,n),满足:

x + n − 1 ≤ H x+n-1\le H x+n1H y + n − 1 ≤ W y+n-1\le W y+n1W,以 ( x , y ) (x,y) (x,y) 为左上角、 ( x + n − 1 , y + n − 1 ) (x+n-1,y+n-1) (x+n1,y+n1) 为右下角的矩阵每个位置都没有洞

H , W ≤ 3000 H,W\le 3000 H,W3000,洞的个数不超过 1 0 5 10^5 105


思路

动态规划 4 4 4 步:

  1. 状态的定义
  2. 转移的推导
  3. 边界的确定
  4. 结果的输出
状态的定义

我们不难想到设 f i , j f_{i, j} fi,j 表示以 ( i , j ) (i, j) (i,j) 正方形右下角的方案数。(这并不是本题的难点)

转移的推导(难点)

对于每一个 f i , j f_{i, j} fi,j,我们考虑他的推导:

  • 如果 A i , j A_{i, j} Ai,j 为洞,那么不会有合法的正方形,故 f i , j = 0 f_{i, j}=0 fi,j=0
  • 如果 A i , j A_{i, j} Ai,j 不为洞,那么考虑从三个方向转移:
    • 那么 f i , j = min ⁡ ( f i − 1 , j , f i , j − 1 , f i − 1 , j − 1 ) + 1 f_{i, j} = \min(f_{i - 1, j}, f_{i, j - 1}, f_{i - 1, j - 1}) + 1 fi,j=min(fi1,j,fi,j1,fi1,j1)+1,首先,这里的 + 1 +1 +1 就是会多出 i , j i, j i,j 自己作为一个正方形,对于取 min ⁡ \min min 的段落,就是通过 3 3 3 个点来确定出从 ( i , j ) (i, j) (i,j) 这个点可以向外延伸多长,下面有几个误区澄清一下:
    • 误区1:有些人会考虑取最大值
      • 这是不对的,考虑设 x 1 x_1 x1 为以 i − 1 , j i - 1, j i1,j 为右下角的合法正方形的最大边长, x 2 x_2 x2 为以 i − 1 , j − 1 i - 1, j - 1 i1,j1 为右下角的合法正方形的最大边长, x 3 x_3 x3 为以 i − 1 , j − 1 i - 1, j - 1 i1,j1 为右下角的合法正方形的最大边长。其实 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3 就是我们的 f i − 1 , j , f i , j − 1 , f i − 1 , j − 1 f_{i - 1, j}, f_{i, j - 1}, f_{i - 1, j - 1} fi1,j,fi,j1,fi1,j1,因为如果边长为 x 1 x_1 x1的正方形是合法的,那么比 x 1 x_1 x1 边长小的正方形也一定合法,又因为我们取得是最大边长,所以就等同于 f i − 1 , j , f i , j − 1 , f i − 1 , j − 1 f_{i - 1, j}, f_{i, j - 1}, f_{i - 1, j - 1} fi1,j,fi,j1,fi1,j1。之后,我们考虑假设 x 1 x_1 x1 最大,那难道答案就是 x 1 x_1 x1吗?当然不是,因为 ( i − x 3 , j − x 3 ) (i - x_3, j -x_3) (ix3,jx3)这个点一定是有洞的,那么如果按我们的理解从 ( i − x 1 , j − x 1 + 1 ) (i-x_1, j-x_1 + 1) (ix1,jx1+1) ( i , j ) (i,j) (i,j)都是无洞的。但是 i − x 1 < i − x 3 i-x_1 < i-x_3 ix1<ix3 j − x 1 + 1 ≤ j − x 3 j-x_1 + 1\le j-x_3 jx1+1jx3,所以一定包含点 ( i − x 3 , j − x 3 ) (i - x_3, j -x_3) (ix3,jx3),即答案不是 x 1 x_1 x1。以此类推,我们就可以证明出答案其实是最小值
    • 误区2:有些人会认为可以不加 ( i − 1 , j − 1 ) (i-1,j-1) (i1,j1)
      • 这个很好反驳,给个例子就行了
        在这里插入图片描述
        假设,我们当前枚举到了 D 4 D4 D4,那么如果光通过 D 3 D3 D3 C 4 C4 C4 转移,方案数为 2 2 2,不过真的对吗?其实是错的因为 B 2 B2 B2 这个点并没有在状态之内,就会发生状态的遗漏。所以,必须加入 ( i − 1 , j − 1 ) (i-1,j-1) (i1,j1),这样才能保证,正方形区域内全是 1 1 1
边界的确定

貌似不需要

结果的输出

最终答案不难想,就是以每个 ( i , j ) (i, j) (i,j) 为正方形右下角的方案数之和,即 r e s = ∑ i = 1 n ∑ j = 1 m f i , j res=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}f_{i,j} res=i=1nj=1mfi,j

一首小诗总结一下

代码
#include <iostream>
#define int long long

using namespace std;

const int N = 3e3 + 10;

int n, m, k;
int a, b;
int mp[N][N], dp[N][N];

signed main()
{
	cin >> n >> m >> k;
	
	for (int i = 1; i <= k; i ++)
			cin >> a >> b, mp[a][b] = 1;
			
	int res =0 ;
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= m; j ++)
		{
			dp[i][j] = min(dp[i - 1][j], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
			if (mp[i][j])
				dp[i][j] = 0;
			res += dp[i][j];
		}
		
	cout << res << endl;
	
	return 0;
}
时间复杂度: O ( n m ) O(nm) O(nm)

恭喜你,成功战胜了 A B C 311 E − D e f e c t   f r e e   S q u a r e s \color{F9E55D}{ABC311E - Defect\ free\ Squares} ABC311EDefect free Squares,奖励继续看题~~~


第五战: C S E S − 1638   G r i d   P a t h s \color{97EF6A}{CSES-1638\ Grid\ Paths} CSES1638 Grid Paths

题目描述
题目大意

给出一个 n × n n\times n n×n 的网格,其中 * 表示当前点不可走,.表示当前点可走,每次只能向右或向左走,问从 ( 1 , 1 ) (1, 1) (1,1) 走到 ( n , n ) (n,n) (n,n) 的方案数,对 1 0 9 + 7 10^9+7 109+7 取模。


思路

动态规划莫过于 4 4 4 步:

  1. 状态的定义
  2. 转移的推导
  3. 边界的确定
  4. 结果的输出
状态的定义:

可以设 f i , j f_{i, j} fi,j 表示从 ( 1 , 1 ) (1,1) (1,1) 走到 ( i , j ) (i,j) (i,j) 的方案数。


转移的推导

比较简单,直接写吧:

f i , j = { f i , j + f i − 1 , j G i − 1 , j ≠ ∗ f i , j + f i , j − 1 G i , j − 1 ≠ ∗ f_{i,j}=\begin{cases} & f_{i,j}+f_{i-1,j} & G_{i-1,j}\ne*\\ & f_{i,j} + f_{i,j-1} & G_{i,j-1}\ne* \end{cases} fi,j={fi,j+fi1,jfi,j+fi,j1Gi1,j=Gi,j1=


边界的确定

f 1 , 1 = 1 f_{1,1}=1 f1,1=1


结果的输出

r e s = f n , n res=f_{n,n} res=fn,n

一首小诗总结一下

起点至坐标 ( i , j ) (i, j) (i,j),方案设为 f i , j f_{i, j} fi,j
转移先判能否转,之后再加方案数。
边界此时要确定,起点自身 1 1 1 方案。
输出终点方案数,天空巨响 A C AC AC


代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 1e3 + 10, mod = 1e9 + 7;

int n;
char g[N][N];
int f[N][N];

signed main()
{
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);

	cin >> n;

	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= n; j ++)
			cin >> g[i][j];

	if (g[1][1] != '*') f[1][1] = 1; //注意:如果第一个点是*就不能在往后推导了
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= n; j ++)
		{
			if (g[i][j] == '*') continue;
			int Way1 = f[i - 1][j], Way2 = f[i][j - 1];
			if (g[i - 1][j] == '*') Way1 = 0;
			if (g[i][j - 1] == '*') Way2 = 0;
			f[i][j] = (f[i][j] + Way1 + Way2) % mod;
		}

	cout << f[n][n] << endl;

	return 0;
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)

恭喜您,成功战胜了 C S E S − 1638   G r i d   P a t h s \color{97EF6A}{CSES-1638\ Grid\ Paths} CSES1638 Grid Paths,奖励无:


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值