线性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()
#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就直接更新lenlen = 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() {qioint 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() {qioint 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,起点和终点都是0dp[1][0] = 0;//从小集合扩展到大集合,集合用S的二进制表示for(int S = 1; S < (1 << n); S++){//枚举点jfor(int j = 0; j < n; j++){//如果S中有j这个点if((S >> j) & 1){//枚举到达j的点kfor(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() {qioint 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() {qioint 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){//分解数字xint 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 = 3int up = (limit ? num[pos] : 9);for(int i = 0; i <= up; i++){//计算000-099if(i == 0 && lead) ans += dfs(pos - 1, sum, true, limit && i == up);//计算200-299else if(i == now) ans += dfs(pos - 1, sum + 1, false, limit && i == up);//计算100-199else 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() {qioint 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() {qioint 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() {qioint 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() {qioint T = 1;
// cin >> T;while (T--) solve();
}