线性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就直接更新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();
}