ET算法【动态规划】

线性dp:dp[i][j] 由 dp[i - 1][j] 通过加减乘除等线性运算得到

状压dp:dp[i][j] 表示一个用二进制数表示的子集来反映当前状态,如7 =(111)(选了三个)

期望dp:dp[i][j] 表示期望或者概率

存在性dp:dp[i][j] 表示目标状态是否存在

树形dp:通过树状结构来状态转移,通常用到DFS

数位dp:[1,n]之中包含多少个69

一、线性DP

1.最长上升子序列

(1)基础版

P1105 - 最长上升子序列(easy) - ETOJ (eriktse.com)

找到一个数组中一直增大的最长子序列(可以不连续),对于每一个点,要么作为起点,要么在左边找一个点连接,因此可以用线性dp,dp[ i ]表示到i点时的最大子序列长度。

复杂度为O(n^{2})

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

const int N = 1e3 + 10;
ll a[N], dp[N];

int main() {
	int n; cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= n; i++){
		dp[i] = 1;
		for(int j = 1; j < i; j++){
			if(a[i] >= a[j]) dp[i] = max(dp[i], dp[j] + 1);
		}
	}
	cout << *max_element(dp + 1, dp + 1 + n) << '\n';
}

(2)贪心 + 二分优化

用一个Array数组记录当前的最长上升子序列,长度为len,从头到尾遍历a[i]数组

①若a[i]  >  Array[len],Array[++len] = a[i]

②若a[i] <= Array[len],此时把Array中第一个大于a[i]的元素替换为a[i],这样保证的是子序列中的元素尽可能的小,则后面进入的元素尽可能的多

    int n; cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	int len = 0;//记录当前最长上升序列的长度
	for(int i = 1; i <= n; i++){
        //返回第一个大于等于a[i]的下标
		int cnt = lower_bound(ans + 1, ans + len + 1, a[i]) - ans;
		ans[cnt] = a[i];
        //如果此时cnt大于len就直接更新len
		len = max(cnt, len);	
	}
	cout << len << endl;

2.最长公共子序列

给定字符串a,b,求最长公共子序列,a有n个字符,b有m个字符,则答案为dp[n][m]

复杂度为O(nm)

转移方程:

① 若a[i] == b[j]  dp[i][j] = dp[i - 1][j - 1] + 1

② 若a[i] !=  b[j]  dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])

3.Problem - 467C - Codeforces

思维题

#include<bits/stdc++.h>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 5e3 + 10;
ll a[N], pre[N], dp[N][N];

void solve() {
	int n, m, k; cin >> n >> m >> k;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= n; i++) pre[i] = pre[i - 1] + a[i];
	dp[1][1] = a[1];
	for(int i = m; i <= n; i++){
		for(int j = 1; j <= k; j++){
			dp[i][j] = max(dp[i - 1][j], dp[i - m][j - 1] + pre[i] - pre[i - m]);
		}
	}
	cout << dp[n][k] << '\n';
}


signed main() {
	qio
	int T = 1;
//	cin >> T;
	while (T--)	solve();
}

4.Problem - 788A - Codeforces

思维题,dp[i][0]存正贡献,dp[i][1]存负贡献

#include<bits/stdc++.h>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;
#define int ll
const int N = 1e5 + 10;
int a[N], d[N], dp[N][2];

void solve() {
	int n; cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= n - 1; i++) d[i] = abs(a[i + 1] - a[i]);
//	for(int i = 1; i <= n; i++) cout << d[i] << ' ';
	for(int i = 1; i <= n - 1; i++){
		dp[i][0] = max(dp[i - 1][1] + d[i], d[i]);
		dp[i][1] = max(dp[i - 1][0] - d[i], 0ll);
	}
	int ans = 0;
	for(int i = 1; i <= n - 1; i ++){
		ans = max(ans, max(dp[i][0], dp[i][1]));
	}
	cout << ans ;
}


signed main() {
	qio
	int T = 1;
//	cin >> T;
	while (T--)	solve();
}

二、状压DP

一般用一个01字符串来表示各个点的状态,就是将一种情况压缩为一个数字或者字符来表示这种情况,这些数字或者字符形成的字符串即为总体情况的状态

1.最短Hamilton路径

P10447 最短 Hamilton 路径 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

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

const int N = 1e5 + 10;
int dp[1 << 20][21];
int dist[21][21];

int main() {
	memset(dp, 0x3f, sizeof dp);
	int n; cin >> n;
	for(int i = 0; i < n; i++){
		for(int j = 0; j < n; j++){
			cin >> dist[i][j];
		}
	}
	//开始:集合中只有点0,起点和终点都是0
	dp[1][0] = 0;
	//从小集合扩展到大集合,集合用S的二进制表示
	for(int S = 1; S < (1 << n); S++){
		//枚举点j
		for(int j = 0; j < n; j++){
			//如果S中有j这个点
			if((S >> j) & 1){
				//枚举到达j的点k
				for(int k = 0; k < n; k++){
					//S ^ (1 << j):S中去掉j点,  >> k & 1 去掉j点后S中所有为1的点
					if((S ^ (1 << j)) >> k & 1){
						dp[S][j] = min(dp[S][j], dp[S ^ (1 << j)][k] + dist[k][j]);
					}
				}
			}
		}
	}
	//dp[(1 << n) - 1][n - 1]即为包含所有的点,终点为n - 1的最短路径
	cout << dp[(1 << n) - 1][n - 1] << '\n';
}

三、区间DP

区间DP中dp[i][j]表示的即为区间i - j内种合法的个数,数据范围通常较小,转移方程一般为:

① dp[i][j] = dp[i + 1][j]

② dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]),  i <= k < j

经典问题为取石子问题。

初始化一般为for(int i = 1; i <= n; i++) dp[i][i] = 0;

1.Problem - 2476 (hdu.edu.cn)

#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 1e2 + 10;
const int inf = 0x3f3f3f3f;
int dp[N][N];

void solve() {
	string a, b;
	while(cin >> a >> b){
		if(a == "11") break;
		int n = a.length();
		a = ' ' + a, b = ' ' + b;
		for(int i = 1; i <= n; i++) dp[i][i] = 1;
		for(int len = 2; len <= n; len++){
			for(int i = 1; i <= n - len + 1; i++){
				int j = i + len - 1;
				dp[i][j] = inf;
				if(b[i] == b[j]){
					dp[i][j] = dp[i + 1][j];
				}
				else{
					for(int k = i; k < j; k++){
						dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
					}
				}
			}
		}
		for(int j = 1; j <= n; j++){
			if(a[j] == b[j]){
				dp[1][j] = dp[1][j - 1];
			}else{
				for(int k = 1; k < j; k++){
					dp[1][j] = min(dp[1][j], dp[1][k] + dp[k + 1][j]);
				}
			}
		}
		cout << dp[1][n] << '\n';;
	}
}


signed main() {
	qio
	int T = 1;
//	cin >> T;
	while (T--)	solve();
}

2.2955 -- Brackets (poj.org)

#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 1e2 + 10;
const int inf = 0x3f3f3f3f;
int dp[N][N];
string s;

bool isok(int i, int j){
	if(s[i] == '[' && s[j] == ']') return true;
	if(s[i] == '(' && s[j] == ')') return true;
	return false;	
}

void solve() {	
	while(cin >> s){
		if(s == "end") break;
		int n = s.length();	
		s = ' ' + s;
		
//		cout << "len:" << n <<'\n';
		memset(dp, 0, sizeof dp);
		for(int len = 2; len <= n; len++){
			for(int i = 1; i <= n - len + 1; i++){
				int j = i + len - 1;
				if(isok(i, j)){
					dp[i][j] = dp[i + 1][j - 1] + 2;
				}
				for(int k = i; k < j; k++){
					dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j]);
				}
				
			}
		}
		cout << dp[1][n] << '\n';
	}
}


signed main() {
	qio
	int T = 1;
//	cin >> T;
	while (T--)	solve();
}

四、数位DP

原理没有太懂,这里记一下板子

P2602 [ZJOI2010] 数字计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

1.递推实现

#include<bits/stdc++.h>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0),cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 15;
ll ten[N], dp[N];
ll cnta[N], cntb[N]; //cnt[i]统计数字i出现了多少次
ll num[N];

void init(){
	ten[0] = 1;
	for(int i = 1; i <= N; i++){
		//ten[i]:10的i次方
		dp[i] = dp[i - 1] * 10 + ten[i - 1]; 
		ten[i] = 10 * ten[i - 1];
	}
}

void solve(ll x, ll* cnt){
	//分解数字x
	int len = 0;
	while(x){
		num[++len] = x % 10;
		x /= 10;
	}
	//从高到低处理x的每一位
	for(int i = len; i >= 1; i--){
		for(int j = 0; j <= 9; j++){
			cnt[j] += dp[i - 1] * num[i];
		}
		//特判最高位比num[i]小的数字
		for(int j = 0; j < num[i]; j++){
			cnt[j] += ten[i - 1];
		}
		//特判最高位的数字num[i]
		ll num2 = 0;
		for(int j = i - 1; j >= 1; j--){
			num2 = num2 * 10 + num[j];
		}
		cnt[num[i]] += num2 + 1;
		cnt[0] -= ten[i - 1];
	}	
}

signed main() {
	init();
	ll a, b; cin >> a >> b;
	solve(a - 1, cnta);
	solve(b, cntb);
	for(int i = 0; i <= 9; i++) cout << cntb[i] - cnta[i] << " ";
}

2.记忆化搜索实现

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

const int N = 15;
ll dp[N][N];
int num[N], now; //now:当前统计0-9的哪一个数字

//pos:当前处理到第pos位
ll dfs(int pos, int sum, bool lead, bool limit){
	ll ans = 0;
	//递归到第0位就返回结果
	if(pos == 0) return sum;
	//记忆化搜索
	if(!lead && !limit && dp[pos][sum] != -1) return dp[pos][sum];
	//这一位的最大值,如324的第3位是up = 3
	int up = (limit ? num[pos] : 9);
	for(int i = 0; i <= up; i++){
		//计算000-099
		if(i == 0 && lead) ans += dfs(pos - 1, sum, true, limit && i == up);
		//计算200-299
		else if(i == now) ans += dfs(pos - 1, sum + 1, false, limit && i == up);
		//计算100-199
		else if(i != now) ans += dfs(pos - 1, sum, false, limit && i == up);
	}
	//状态记录:有前导0,无数位限制
	if(!lead && !limit) dp[pos][sum] = ans;
	return ans;
}

ll solve(ll x) {
	int len = 0;
	while(x){
		num[++len] = x % 10;
		x /= 10;
	}
	memset(dp, -1, sizeof dp);
	return dfs(len, 0, true, true);
}

signed main() {
	ll a, b; cin >> a >> b;
	for(int i = 0; i < 10; i++) now = i, cout << solve(b) - solve(a - 1) << " ";
	return 0;
}

五、树形DP

即在树上用DP来维护最值,因为树上的子树天然满足dp的递归性质,一般用dp[i][j],i表示节点,j表示题目要求的条件,使用dfs进行递推转移

1.Problem - 1926G - Codeforces

因为C的状态不确定而又对其他的点有影响,所以需要维护三个状态的dp

#include<bits/stdc++.h>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 2e5 + 10;
vector<int> g[N];
ll dp[N][3]; //dp[][0]都没有,dp[][1]在睡觉,dp[][2]在嗨
char s[N];

void dfs(int now){
	dp[now][0] = dp[now][1] = dp[now][2] = 0;
	for(auto nex : g[now]){
		dfs(nex);
		if(s[nex] == 'S'){
			dp[now][1] += dp[nex][1];
			dp[now][2] += dp[nex][1] + 1;
			dp[now][0] = 1e9;
		}
		if(s[nex] == 'P'){
			dp[now][1] += dp[nex][2] + 1;
			dp[now][2] += dp[nex][2];
			dp[now][0] = 1e9;
		}
		if(s[nex] == 'C'){
			dp[now][1] += min(dp[nex][1], dp[nex][2] + 1);
			dp[now][2] += min(dp[nex][1] + 1, dp[nex][2]);
			dp[now][0] = max(dp[now][0], dp[nex][0]);
		}
	}
	if(s[now] == 'S') dp[now][0] = dp[now][2] = 1e9;
	if(s[now] == 'P') dp[now][0] = dp[now][1] = 1e9;
}

void solve() {
	int n; cin >> n;
	for(int i = 1; i <= n; i++) g[i].clear();
	for(int i = 2; i <= n; i++){
		int x; cin >> x;
		g[x].push_back(i);
	}
	for(int i = 1; i <= n; i++) cin >> s[i];
	dfs(1);
	cout << min(dp[1][0], min(dp[1][1], dp[1][2])) << '\n';
}


signed main() {
	qio
	int T = 1;
	cin >> T;
	while (T--)	solve();
}

2.Problem - 1561 (hdu.edu.cn)

类似区间DP,枚举每颗子树now上分别分有j : 0 ~ m条边的情况,再枚举now子树nex上分别有k : 0 ~ j - 1条边的情况

#include<iostream>
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 2e2 + 10;
int n, m;
struct node{
	int v;
	ll w;
};
vector<node> g[N];
ll dp[N][N];//第i个节点,选了j条边时的最大值
int sum[N];//以i为根节点的子树中边的个数
void dfs(int now){
	for(int i = 0; i < g[now].size(); i++){
		int nex = g[now][i].v, w = g[now][i].w;
		dfs(nex);
		sum[now] += sum[nex] + 1;
		for(int j = min(m, sum[now]); j >= 0; j--){
			for(int k = 0; k <= min(sum[nex], j - 1); k++){
				dp[now][j] = max(dp[now][j], dp[now][j - k - 1] + dp[nex][k] + w);
			}
		}
	}
}

void solve() {
	while(cin >> n >> m){
		if(n == 0 && m == 0) break;
		memset(dp, 0, sizeof dp);
		memset(sum, 0, sizeof sum);
		for(int i = 0; i <= n; i++) g[i].clear();
		for(int i = 1; i <= n; i++){
			int u; cin >> u;
			ll w; cin >> w;
			g[u].push_back({i, w});
		}
		dfs(0);
		cout << dp[0][m] << '\n';
	}
	
}


signed main() {
	qio
	int T = 1;
//	cin >> T;
	while (T--)	solve();
}

六、存在性DP

比较简单

1.Problem - 1472B - Codeforces

#include<bits/stdc++.h>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 1e3 + 10;
int a[N], dp[N];

void solve() {
	memset(dp, 0, sizeof dp);
	int n; cin >> n;
	int sum = 0;
	for(int i = 1; i <= n; i ++){
		cin >> a[i];
		sum += a[i];
	}
	if(sum % 2 == 1){
		cout << "NO\n";
		return;
	}
	dp[0] = 1;
	for(int i = 1; i <= n; i++){
		for(int j = sum / 2; j >= a[i]; j--){
			dp[j] |= dp[j - a[i]];
		}
	}
	if(dp[sum / 2]) cout << "YES\n";
	else cout << "NO\n";
}


signed main() {
	qio
	int T = 1;
	cin >> T;
	while (T--)	solve();
}

七、记忆化搜索

在DFS回溯的过程中,更新从当前格子出发能够得到的最大价值,以避免重复的计算。
因为回溯时,上一个格子的所有可能情况都已经考虑过了,也就是说上一个格子的状态已经是最优的了,所以直接用上一个格子的值来更新当前格子。在当前格子的所有方向都回溯完时,当前格子也就达到了最优值,继续更新之后的。

1.Problem - 1078 (hdu.edu.cn)

#include<iostream>
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
#define qio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
typedef long long ll;
typedef double db;

const int N = 1e2 + 10;
int mp[N][N], dp[N][N];
int n, k;
int xx[10] = {1, 0, -1, 0}, yy[10] = {0, 1, 0, -1};
bool isok(int x, int y){
	if(1 <= x && x <= n && 1 <= y && y <= n) return true;
	else return false;
}
int dfs(int nowx, int nowy){
	int res = 0;
	if(!dp[nowx][nowy]){
		for(int i = 0; i < 4; i++){
			for(int j = 1; j <= k; j++){
				int nexx = nowx + xx[i] * j, nexy = nowy + yy[i] * j;
				if(isok(nexx, nexy) && mp[nexx][nexy] > mp[nowx][nowy]){
					res = max(res, dfs(nexx, nexy));
				} 
			}
		}
		dp[nowx][nowy] = res + mp[nowx][nowy];
	}
	return dp[nowx][nowy];
}

void solve() {	
	while(cin >> n >> k){
		if(n == -1 && k == -1) break;
		memset(dp, 0, sizeof dp);
		for(int i = 1; i <= n; i++){
			for(int j = 1; j <= n; j++){
				cin >> mp[i][j];
			}
		}
		
		cout << dfs(1, 1) << '\n';
	}
}


signed main() {
	qio
	int T = 1;
//	cin >> T;
	while (T--)	solve();
}

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值