一、何为DP
DP,全称动态规划,是解决一类决策最优解的问题的方法。
详情请见:
理论篇---->动态规划_百度百科 (baidu.com)
现实篇---->【算法 - 动态规划】从零开始学动态规划!(总纲) - 知乎 (zhihu.com)
二、DP的分类
I 基本DP
基本DP,都包含线性DP、区间DP等,这里不过多介绍了。
II 树形DP
树形DP,顾名思义,一个在树上进行的DP。
主要推导方式是沿着根节点往下遍历,在回溯过来,在回溯过程中去进行DP的过程。
例题呈现 - 没有上司的舞会
题目大意
给定个节点的数值,选取一定的节点计算数值总和,如果节点的父亲节点被选取了,那么节点将不能被选择。
题目解析
本体是一个很典型的树形DP的题目。
可以设,为当前记录到的节点编号,为当前节点的选取状态(0为不选取,1为选取)。
简单思考后就可以推理出状态转移方程:
为当前遍历到的节点,为节点的子节点。
如果节点没被选取,那么节点就选取选取与不选取两种情况的最大值即可。
如果节点被选取,那么节点只能选取不选取的情况,记住还要加上当前节点的数值共享。
正解代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
// #include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define bug printf("--------\n");
#define enter printf("\n");
#define debug(x) cout << #x << '=' << x << endl;
#define file(FILENAME) \
freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout);
#define rep(i, a, b) for (int(i) = (a); (i) <= (b); ++(i))
#define inv(x, mod) ppow((x), (mod)-2)
#define pb push_back
#define mk make_pair
#define mem(t, v) memset((t), (v), sizeof(t));
#define DBG cerr << __LINE__ << ' ' << __FUNCTION__ << endl;
#define CLOSE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define N 6005
#define M 100005
#define inf 0x3f3f3f3f3f3f3f3f
#define mod 1000000009
ll n;
struct node{
ll num;
ll father;
ll kid;
ll kids[N];
}tree[N];
ll f[N][2];
void dfs(ll x){
if(tree[x].kid==0){
f[x][0]=0;
f[x][1]=tree[x].num;
return ;
}
for(ll i=1;i<=tree[x].kid;i++){
dfs(tree[x].kids[i]);
f[x][0]+=max(f[tree[x].kids[i]][0],f[tree[x].kids[i]][1]);
f[x][1]+=f[tree[x].kids[i]][0];
}
f[x][1]+=tree[x].num;
}
int main() {
// file("")
// CLOSE
cin>>n;
for(int i=1;i<=n;i++){
cin>>tree[i].num;
}
for(int i=1;i<=n-1;i++){
ll a,b;
cin>>a>>b;
tree[a].father=b;
tree[b].kids[++tree[b].kid]=a;
}
for(int i=1;i<=n;i++){
if(tree[i].father==0){
dfs(i);
cout<<max(f[i][0],f[i][1]);
return 0;
}
}
return 0;
}
III 换根DP
换根DP,是树形DP的一种拓展处理,主要用于处理树形DP存在不定根时的情况。
主要推导方式是先选取任意一点作为根节点,进行一遍树形DP的处理,然后再根据题目的要求,利用已经求出来的一个节点作为根节点的答案去推导其他的答案。
例题呈现 - STA-Station
题目大意
给定一个个点的树,找到一个节点为根节点时,所有结点的深度之和最大。
题目解析
第一遍的树形DP的状态转移方程很好推导,如下:
为当前遍历到的节点,为节点的子节点,为以为根节点时的子树大小。
同时,也需要记录啊,状态转移方程如下:
最终完成后,我们得到了以一个节点为根节点时的答案。那我们接下来该怎么办?要一个一个点再去跑一遍这个过程吗?很显然,不行滴!
那我们可以这样考虑,假设第一个当作根节点的节点编号为1,它有一个子节点2,我们可以旋转一下,把2提取到根节点上来,1变为2的子节点,以此类推得到答案。
由此可得 .
类比得知:.
化简后的:
这样就可以得到全部节点的答案了,找到最大值输出即可。
正解代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
// #include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define bug printf("--------\n");
#define enter printf("\n");
#define debug(x) cout << #x << '=' << x << endl;
#define file(FILENAME) \
freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout);
#define rep(i, a, b) for (int(i) = (a); (i) <= (b); ++(i))
#define inv(x, mod) ppow((x), (mod)-2)
#define pb push_back
#define mk make_pair
#define mem(t, v) memset((t), (v), sizeof(t));
#define DBG cerr << __LINE__ << ' ' << __FUNCTION__ << endl;
#define CLOSE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define N 1000005
#define M 100005
#define inf 0x3f3f3f3f3f3f3f3f
#define mod 1000000009
ll n;
ll g[N];
ll f[N];
ll tree_size[N];
vector<ll> edge[N];
void dfs_g(ll u, ll fa) {
tree_size[u] = 1;
for (ll v : edge[u]) {
if (v == fa) continue;
dfs_g(v, u);
g[u] += (g[v] + tree_size[v]);
tree_size[u] += tree_size[v];
}
}
void dfs_f(ll u, ll fa) {
for (ll v : edge[u]) {
if (v == fa) continue;
f[v] = f[u] - 2 * tree_size[v] + n;
dfs_f(v, u);
}
}
int main() {
// file("")
// CLOSE
cin >> n;
for (int i = 1; i <= n - 1; i++) {
ll l, r;
cin >> l >> r;
edge[l].pb(r);
edge[r].pb(l);
}
dfs_g(1, 0);
f[1] = g[1];
dfs_f(1, 0);
ll maxx = 0;
for (int i = 1; i <= n; i++) {
maxx = max(maxx, f[i]);
}
for (int i = 1; i <= n; i++) {
if (maxx == f[i]) {
cout << i << ' ';
return 0;
}
}
return 0;
}
Ⅳ 状态压缩DP
状态压缩DP(以下称为状压DP),是将一部分状态压缩成一个数字去进行处理的一种DP,一般可以解决在矩阵地图中有限制性的排列一些点的问题。
例题呈现 - 互不侵犯
题目大意
在的棋盘中放置个国王,求有多少种情况。
国王攻击范围:上、下、左、右、左上、左下、右上、右下。
题目解析
本题直接暴力肯定会超时,那么就考虑dp解决。
可以设,为当前遍历到的行数,为当前行放置棋子的状态(用二进制表示,0为没有棋子,1为有棋子),当前已经放置了多少个棋子。
简单推导可得:
为当前行数,为当前行的状态,为当前放置的棋子总数,为枚举出来的上一行的状态,为的状态中包含的棋子数量。
正解代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
// #include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define bug printf("--------\n");
#define enter printf("\n");
#define debug(x) cout << #x << '=' << x << endl;
#define file(FILENAME) \
freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout);
#define rep(i, a, b) for (int(i) = (a); (i) <= (b); ++(i))
#define inv(x, mod) ppow((x), (mod)-2)
#define pb push_back
#define mk make_pair
#define mem(t, v) memset((t), (v), sizeof(t));
#define DBG cerr << __LINE__ << ' ' << __FUNCTION__ << endl;
#define CLOSE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define N 100005
#define M 100005
#define inf 0x3f3f3f3f3f3f3f3f
#define mod 1000000009
ll f[11][601][101];
ll nnum[601];
ll cnt(ll x) {
ll sum = 0;
while (x) {
if (x & 1) sum++;
x >>= 1;
}
return sum;
}
int main() {
// file("a")
// CLOSE
ll n, m;
cin >> n >> m;
for (int i = 0; i <= (1 << n) - 1; i++) {
nnum[i] = cnt(i);
f[1][i][nnum[i]] = 1;
}
for (int i = 2; i <= n; i++) {
for (int p = 0; p <= (1 << n) - 1; p++) {
if (p & (p >> 1)) continue;
for (int j = 0; j <= (1 << n) - 1; j++) {
if (j & (j >> 1)) continue;
if (j & p || j & (p << 1) || j & (p >> 1)) continue;
for (int k = nnum[j]; k <= m; k++) {
f[i][j][k] += f[i - 1][p][k - nnum[j]];
}
}
}
}
ll ans = 0;
for (int j = 0; j <= (1 << n) - 1; j++) ans += f[n][j][m];
cout << ans;
return 0;
}
V 数位DP
数位DP,是解决一类在一个区间内选取满足要求的数字的一种方式。