动态规划

动态规划基础

线性DP

DP(动态规划)

全称Dynamic Programming,将复杂问题分解为重叠子问题(与[[DFS]]不同),并通过子问题的解得到整个问题的解的算法。

状态

形如dp[i][j] = val的取值,其中i,j为下标,也是用于描述,确定状态所需的变量,val为状态值。

状态转移

状态与状态间的转移关系,一般可表示为一个数学表达式,转移方向确定迭代与递归方向。

最终状态

就是最后的答案。

分析步骤

确定状态

一般为到第几个为止的方案数/最小代价/最大价值。

确定状态转移方程

即从已知状态得到新状态的方法,并确保这个方向一定可以正确得到最终状态。根据状态转移决定使用迭代法还是递归法。

确定最终状态

输出

例题

数字三角形
状态转移方程:dp[i][j] = max( dp[i+1][j] , dp[i+1][j+1] ) + a[i][j]
即每次都有两种选择,选择这两种中最优的,而这两种情况也是如此地依次向下寻找答案。
这里用下面的状态更新上面的,所以我们从下往上进行状态转移,最后输出dp[1][1]

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 105;
int a[N][N], dp[N][N];


signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n; cin >> n;
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= i; ++j)
			cin >> a[i][j];

	//以三角形底座为第一层,由第一层往上更新,遍历第二层,找到加到第二层时第二层每个元素对应的子元素最大和
	//然后遍历第三层,找到第三层每个元素对应的子元素最大和,依次进行,直到dp[i][j]中ij都为1,得到答案
	for (int i = n; i >= 1; --i)
		for (int j = 1; j <= i; ++j)
			dp[i][j] = a[i][j] + max(dp[i + 1][j], dp[i + 1][j + 1]);

	cout << dp[1][1] << '\n';
	return 0;
}

破损的楼梯
每次走一级或两级,设状态dp[i]表示走到第i级台阶的方案数。
则走到第i级的上一步有两种情况(一步或两步)
状态转移方程dp[i] = dp[i-1] + dp[i-2],如果i级台阶是破损的dp[i] = 0
从前向后更新。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 9;
const int P = 1e9 + 7;
int dp[N];
bool broken[N];//存放破损的台阶

signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n, m; cin >> n >> m;
	
	//m个破损的位置
	for (int i = 1; i <= m; ++i) {
		int temp; cin >> temp;
		broken[temp] = true;
	}

	dp[0] = 1;//dp[2-2]=1,且dp[1-1]=1,即从0走一步到1与从0走一步到2的方案数都为1
	if (!broken[1])dp[1] = 1;//第一级单独考虑,因为不能从-1转移得到dp[1]
	for (int i = 2; i <= n; ++i) {
		if (broken[i])continue;
		dp[i] = (dp[i - 1] + dp[i - 2]) % P;//初始化为0,如果上一级或两即损坏了就加0,要记得依题意取模
	}

	cout << dp[n] << '\n';
	return 0;
}

安全序列

flowchart LR 1 --> 2 --> 3 --> 4 --> 5 --> 6 --> 7

dp[i]表示以i结尾的方案总数。
假设至少要2个空位隔开,因为以i处结尾可能的状态总数是由两个空位前的状态得到的,两个空位前的状态又是由两个之前的状态得到(可能存在3,4,5……个空)。
得状态转移方程 $$ dp[i] = \sum\limits_{j=1}^{i-k-1}dp[j] $$
同时利用[[前缀和]]优化时间复杂度。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 9;
const int P = 1e9 + 7;
int dp[N], prefix[N];

signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n, k; cin >> n >> k;
	dp[0] = prefix[0] = 1;//可以全都不放
	for (int i = 1; i <= n; ++i) {
		if (i - k - 1 < 1)dp[i] = 1;
		else dp[i] = prefix[i - k - 1];
		prefix[i] = (prefix[i - 1] + dp[i]) % P;
	}
	cout << prefix[n] << '\n';
	return 0;
}

二维DP

就是有多个维度的dp。

例题

建造房屋

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 1e9 + 7;

int dp[55][2000];//ij,到当前街道i共建造j个房屋
/*
dp[i][j+k] = (dp[i][j+k] + dp[i-1][k])%mod
*/
signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, m, k; cin >> n >> m >> k;
	for (int i = 0; i <= k; i++)dp[0][i] = 1;//初始化,第0条街道,建i个房屋的方案1数只有1

	for (int i = 1; i <= n; i++)//前1至n条街道
	{
		for (int j = 1; j <= m; j++)//考虑在当前街道依次建造1至m个房屋
		{
			for (int x = i - 1; x <= k-1; x++)
			//对于前i-1条街道,总共至少建i-1个房屋,至多x-1个房屋
			//遍历前i-1条街道可能的房屋总数x
			{
				//当前街道建j间房前i-1街道建x间房,故前i条街道建j+x个房
				dp[i][j + x] = (dp[i - 1][x] + dp[i][j + x]) % mod;
			}
		}
	}
	cout << dp[n][k] << '\n';
	return 0;
}

摆花
状态转移方程:

\[dp[i][j] = \sum\limits_{k=0}^{a[i]}dp[i-1][j-k] \]

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 1e6 + 7;
const int N = 105;
int a[N],dp[N][N];

signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n, m; cin >>n >> m;
	dp[0][0] = 1;
	for (int i = 1; i <= n; i++){
		cin >> a[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			for (int k = 0; k <= a[i] && k <= j; k++) {
				dp[i][j] = (dp[i][j] + dp[i - 1][j - k]) % mod;//一共j个,这一次放k个,上一个就放j-k个
			}
		}
	}
	cout << dp[n][m] << '\n';
	return 0;
}

地图

#include <bits/stdc++.h>
using namespace std;
const int N = 1e2+5;
char pic[N][N];
int n,m,k;
int dp[N][N][2][8];//要同时记录走到这一点的方向与拐弯次数,单单记录地图位置是不全面的

bool ok(int a,int b){
  if(a>n||b>m)return false;
  else if(pic[a][b]=='#')return false;
  else return true;
}

int dfs(int row,int col,int x,int num){
  int cnt = 0;
  //递归出口
  if(!ok(row,col))return 0;
  if(row==n&&col==m)return 1;

  //记忆化搜素
  if(dp[row][col][x][num])return dp[row][col][x][num];

  //转向没到k次
  if(num<k){
    //下false表示下
    cnt = cnt + dfs(row+1,col,0,x==0?num:num+1);
    //右true表示右
    cnt = cnt + dfs(row,col+1,1,x==1?num:num+1);//判断有无变方向
  }

  //到k次了
  if(num==k){
    if(x==0)cnt = cnt + dfs(row+1,col,0,num);
    else cnt = cnt + dfs(row,col+1,1,num);
  }
  dp[row][col][x][num] = cnt;
  return cnt;
}

int main(){
  cin>>n>>m>>k;
  for(int i=1;i<=n;++i){
    for(int j=1;j<=m;++j)cin>>pic[i][j];
  }

  int ans = 0;
  ans = dfs(1,2,1,0)+dfs(2,1,0,0);
  cout<<ans<<'\n';
  return 0;
}

最长上升子序列LIS

子序列

是指一个序列中按照原顺序选出若干个不一定连续的元素组成的序列。

朴素LIS模型

O(n^2)时间复杂度
求解LIS时设dp[i]表示1~i序列中以a[i]结尾的最长上升子序列的长度,必须以a[i]结尾,因为后续要比较大小。
可得状态转移方程为:

\[dp[i] = max(dp[i],dp[j] + 1),if(a[i] > a[j]) \]

表示a[i]插入到不同子序列后的情况。

例子

a 1 3 4 2 5 3 7
dp 1 2 3 2 4 3 5
蓝桥勇士
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3+5;
int num[N];
int dp[N];

signed main(){
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  int n;cin>>n;
  for(int i=1;i<=n;++i)cin>>num[i];

  for(int i=1;i<=n;++i){
    dp[i] = 1;
    for(int j=1;j<=i;++j){
      if(num[i]>num[j])
      dp[i] = max(dp[i],dp[j]+1);//这里是为了找到最大的dp[j]+1,设dp[j]+为x
      //因为每次都取最大值,而dp[i]的最大值又由x得到,所以每次都是x与上一个x比较
      //最终得到最大的x
    }
  }
  int ans = 0;
  for(int i = 1;i <= n;++ i){
	  ans = max(dp[i],ans);
  }
  cout<<ans<<'\n';//输出最大的序列长度
  return 0;
}

最长公共子序列LCS

公共子序列

两个数列,其中相同的子序列为公共子序列
比如A 1 3 2与B 1 2 2的最长公共子序列为1 2

LCS算法模型

O(n^2)的时间复杂度
dp[i][j]表示A[1~i]与序列B[1~j]序列中(不规定结尾)的最长公共子序列的长度
状态转移方程:
if a[i] = b[j]:dp[i][j] = dp[i-1][j-1] + 1
此时可以将他们作为公共元素插入LCS后面,长度加一。
else dp[i][j] = max(dp[i-1][j], dp[i][j-1])

图例

原数组

A 1 3 4 2 5
B 1 4 3 5 2

dp数组,横纵坐标表示原数组下标

1 1 1 1 1
1 1 2 2 2
1 2 2 2 2
1 2 2 2 3
1 2 2 3 3

例题

最长公共子序列

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3 + 5;
int a[N], b[N], dp[N][N];

signed main() {
	int n, m; cin >> n >> m;
	for (int i = 1; i <= n; ++i)cin >> a[i];
	for (int i = 1; i <= m; ++i)cin >> b[i];

	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			if (a[i] == b[j])dp[i][j] = dp[i - 1][j - 1] + 1;
			else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		}
	}

	cout << dp[n][m] << '\n';
	return 0;
}

求出具体子序列

(i,j)(n,m)往回走,当a[i]=b[j]时,往左上角走变为(i-1,j-1),此时找到一个公共元素a[i]b[j],否则走左上或上(选较大方向),重复直到出边界。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3 + 5;
int a[N], b[N], dp[N][N];

signed main() {
	int n, m; cin >> n >> m;
	for (int i = 1; i <= n; ++i)cin >> a[i];
	for (int i = 1; i <= m; ++i)cin >> b[i];

	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			if (a[i] == b[j])dp[i][j] = dp[i - 1][j - 1] + 1;
			else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		}
	}

	vector<int> v;
	int x = n, y = m;
	while (x && y) {
		if (a[x] == b[y]) {
			v.push_back(a[x]);
			x--, y--;
		}
		else if (dp[x - 1][y] > dp[x][y - 1])x--;
		else y--;
	}

	reverse(v.begin(), v.end());//倒置数组
	for (const auto& i : v)cout << i << ' ';
	return 0;
}
posted @ 2024-02-20 00:24  BreadCheese  阅读(32)  评论(0)    收藏  举报