动态规划
所有dp都是由阶段——子问题,状态——一组通解,决策——挑最合算的三要素组成。
一. 背包
1.01背包
对于容量为\(V\)的背包,有\(N\)种物品,每个物品仅有\(1\)个,第\(i\)物品的花费是\(cost_i\),价值是\(val_i\),求最大价值
标准状态转移:
for(int i=1;i<=n;i++){
for(int j=V;j>=cost[i];j--){
dp[j] = max(dp[j],dp[j-cost[i]]+val[i]);
}
}
2.完全背包
对于容量为\(V\)的背包,有\(N\)种物品,每个物品有\(\infty\)个,第\(i\)物品的花费是\(cost_i\),价值是\(val_i\),求最大价值
标准状态转移:
for(int i=1;i<=n;i++){
for(int j=cost[i];j<=V;j++){
dp[j] = max(dp[j],dp[j-cost[i]]+val[i]);
}
}
3.多重背包
对于容量为\(V\)的背包,有\(N\)种物品,每个物品有\(num_i\)个,第\(i\)物品的花费是\(cost_i\),价值是\(val_i\),,求最大价值
标准状态转移:
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=0;k<=num[i];k++){
if(j >= k*cost[i]){
dp[j] = max(dp[j],dp[j - k * cost[i]] + k*val[i]);
}
}
}
}
优化状态转移<全码>:
//
#include<bits/stdc++.h>
using namespace std;
const int maxm = 6e3 + 5;
const int maxn = 500 + 5;
int n,m;
int dp[maxm],c[maxn],v[maxn],s[maxn];
void ZOP(int cost,int val){
for(int i=m;i>=cost;i--){
dp[i]=max(dp[i],dp[i-cost]+val);
}
}
void CP(int cost,int val){
for(int i=cost;i<=m;i++){
dp[i]=max(dp[i],dp[i-cost]+val);
}
}
void MP(int cost,int val,int num){
if(cost*num > m)CP(cost,val);
else {
int t=1;
while(t<num){
ZOP(t*cost,t*val);
num-=t;
t*=2;
}
ZOP(num*cost,num*val);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>c[i]>>v[i]>>s[i];
}
for(int i=1;i<=n;i++){
MP(c[i],v[i],s[i]);
}
cout<<dp[m];
return 0;
}
- 分组背包
一个旅行者有一个最多能装V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
标准状态转移<全码>:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 200 + 5;
int dp[maxn],c[maxn],val[maxn],s[maxn],g[maxn][maxn];
int main(){
int v,n,t,k;
scanf("%d%d%d",&v,&n,&t);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&c[i],&val[i],&k);
s[k]++;int pos=s[k];g[k][pos]=i;
}
for(int i=1;i<=t;i++){
for(int j=v;j>=0;j--){
for(k=1;k<=s[i];k++){
if(j>=c[g[i][k]]){
dp[j]=max(dp[j],dp[j-c[g[i][k]]]+val[g[i][k]]);
}
}
}
}
printf("%d",dp[v]);
return 0;
}
二.状态压缩dp
状态压缩dp,顾名思义,就是将一个状态(最好是由可以用 0 或 1 表示的状态)压缩成一个数,作为dp的一个维度,进行转移。而这类题目的时空限制也较为明显,大多在20以内。而且代码复杂、优先级困扰、转移容易失误也是其明显特点。所以在考场上,除了陷入“进退维谷”的阶段或一步就可以看出的清晰转移,尽量避免使用。但同时,我们更应该努力学习这种算法,因为它几乎是dp进阶的代表作
- 逻辑运算符
- ! 非 !A
- && 与 A && B
- || 与 A || B
- 位运算符 括号最保险
- << >> 左移 右移
- ~ 非 ~A (not) 按位取反
- & 与 A & B (and) 按位与
- ^ 异或 A ^ B (xor) 按位异或
- | 或 A | B (or) 按位或
#include<bits/stdc++.h>
using namespace std;
const int maxn = 20;
const int maxs = 1 << 20;
int dp[maxs][maxn];
//dp的第一维 用一个n位二进制数保存当前到达过哪些城市
//dp的第二维 用一个整数保存当前的位置(城市)
int g[maxn][maxn];//g[i,j]保存i -> j 的距离
int n;
int main(){
cin >> n;
for(int i = 1;i <= n;i ++){
for(int j = 1;j <= n;j ++)
cin >> g[i][j];
}
memset(dp,0x3f,sizeof(dp));
dp[3][2] = 0;
//由于这里是从1开始编号,所以我们直接从3,即(11)_2开始用(其实就是把2的0次幂为置1,然后从(00000…01)推到(11111…11))
//In fact,这里的城市循环都是从1开始的,所以不会影响2的0次幂位
for(int i = 3;i <= (1 << n + 1) - 1;i ++){//循环状态
for(int j = 1;j <= n;j ++){//循环当前的城市
if(((i >> j) & 1) == 0)continue;//如果当前状态表示没有到过当前城市,显然矛盾
for(int k = 1;k <= n;k ++){//循环上一步的城市
if(((i >> k) & 1) == 0)continue;//如果当前状态表示没有到过上一步的城市,显然矛盾
dp[i][j] = min(dp[i][j],dp[i ^ (1 << j)][k] + g[k][j]);
//dp[i,j] = 上一步未到过j城的状态且在k城市的值 + k -> j 的距离
}
}
}
cout << dp[(1 << n + 1) - 1][n];
//所有城市都到过且最后一步在n城
return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int maxs = (1 << 10) + 5;
const int maxn = 10 + 5;
const int maxk = 10 * 10;
long long dp[maxn][maxs][maxk];
//dp[i,j,k]: i 当前进行到的行数
// j 当前进行到的状态的编号
// k 当前使用的国王的数量
long long n,cnt,m;
long long Situation[maxs],Used[maxs];
//Sit[i] : 编号为i的状态(仅1行)
//Used[i] : 编号为i的状态使用的国王的数量
int Get(int i){
int s = 0;
while(i){
s += i % 2;
i = i >> 1;
}
return s;
}
//计算状态i中使用国王的个数(即1的数量)
void init(){
for(int i = 0;i <= (1 << n) - 1;i ++){
if(((i << 1) & i) == 0){
cnt ++;
Situation[cnt] = i;
Used[cnt] = Get(i);
}
}
}
//预处理可以使用的状态
/*
逻辑 : 如果两个国王挨在一起,则不能使用
处理 : 将i左移一位,那么如果有挨在一起的国王(1), & 运算会使他们的结果大于零
所以如果任意两个国王都不挨在一起,那么两个状态的 & 一定 == 0;
证明 : 随意取一个数都可以做到。
失败 : 3
1 1 0
& 0 1 1
------
0 1 0 ,显然有国王在一起,不可
成功 :5
1 0 1 0
& 0 1 0 1
--------
0 0 0 0 , 显然无国王在一起,可行
*/
int main(){
cin >> n >> m;
init();//预处理
for(int i = 1;i <= cnt;i ++){
if(Used[i] <= m){
dp[1][i][Used[i]] = 1;
}
}
/*
第一行的情况:如果第i种状态的国王使用量不超过m个,则可行,方案数为1
*/
for(int i = 2;i <= n;i ++){//第i行
for(int j = 1;j <= cnt;j ++){//当前状态
for(int k = 1;k <= cnt;k ++){//上一步状态
if(Situation[j] & Situation[k])
continue;
if(Situation[j] & (Situation[k] << 1))
continue;
if((Situation[j] << 1) & Situation[k])
continue;
/*
三种情况的王不见王:顶上;左上;右上;
*/
for(int t = 1;t + Used[j] <= m;t ++){
dp[i][j][Used[j] + t] += dp[i - 1][k][t];
//在第i行的第j种状态,此时用了Used[j]个国王,加上上一步用了t个国王的方案数,total就是此时方案数
}
}
}
}
long long ans = 0;
for(int i = 1;i <= n;i ++){
for(int j = 1;j <= cnt;j ++){
ans += dp[i][j][m];
}
} // 所有用了m个国王的方案都是好方案
cout << ans;
return 0;
}
三. 树形dp
树形dp,即在树上(甚至是DAG上)以深度进行划分阶段,由子节点向根一步步进行转移,最终得到根的解。但多数时候是无根树,需要将每个节点都作为根,将最终结果在进行择优。而树形dp又有一个奇特的特点:多数情况下可以用两种方法实现:递归,拓扑。
/*
Name: LGOJ P1352 没有上司的舞会
Author: Jack
Date: 30-07-19 22:45
Description:
本题是树形dp的板子题,第一份代码是拓扑实现,而第二份代码用的是递归。
而他们的逻辑都是一样的——从叶子到分支到根。
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 6000 + 5;
vector <int> next[maxn]; //保存父子关系
int n;
int ind[maxn];//入度(其实除了根节点都是1,但就是用来找根节点的)
int weight[maxn];//每个员工的快乐程度
int path[maxn],cnt;//记录拓扑序
int dp[maxn][2];//dp[i,1]记录以i为根的子树而且i号节点加入舞会的最大快乐程度
void top(int root){
queue<int>q;
q.push(root);
path[++cnt] = root;
while(! q.empty()){
int p = q.front();q.pop();
for(int i = 0;i < next[p].size();i ++){
int nxt = next[p][i];
ind[nxt] --;
if(ind[nxt] == 0){
q.push(nxt);
path[++cnt] = nxt;
}
}
}
// for(int i = 1;i <= cnt;i ++){
// cout << path[i] << " ";
// }
}// 拓扑排出广度优先的拓扑序
void work(){
//按拓扑序倒序dp(想想拓扑序是怎样的)
for(int i = cnt;i >= 1;i --){
int x = path[i];
dp[x][0] = 0;//i不加入则暂时没有快乐
dp[x][1] = weight[x];//i加入则快乐是w[i];
for(int i = 0;i < next[x].size();i ++){
//循环x的儿子们(下属只能当儿子QWQ)
int s = next[x][i];
dp[x][1] += dp[s][0]; //如果i加入,则下属们都不能参加
dp[x][0] += max(dp[s][1],dp[s][0]);
//如果i不加入,则下属们可加可不加
}
}
}
int main(){
cin >> n;
for(int i = 1;i <= n;i ++){
cin >> weight[i];
}
for(int i = 1;i <= n;i ++){
int tmpf,tmps;
cin >> tmps >> tmpf;
if(tmpf == 0 && tmps == 0)
break;
ind[tmps] ++;
next[tmpf] . push_back(tmps);
}//输入,处理儿子关系
int root;
for(int i = 1;i <= n;i ++){
if(ind[i] == 0){
root = i;
break;
}
}//找根
top(root);
work();
cout << max(dp[root][0],dp[root][1]) << endl;
//校长加或不加有两种情况,择优输出
return 0;
}
//递归版
#include<bits/stdc++.h>
using namespace std;
const int maxn = 6000 + 5;
vector <int> next[maxn];
int n;
int ind[maxn];
int weight[maxn];
int dp[maxn][maxn];
//数组含义同上
void work(int pos){
dp[pos][0] = 0;
dp[pos][1] = weight[pos];
//前两步同上
for(int i = 0;i < next[pos].size();i ++){
int nxt = next[pos][i];
work(nxt);//再跑自己以前先跑一把下属
dp[pos][1] += dp[nxt][0];
dp[pos][0] += max(dp[nxt][0],dp[nxt][1]);
}
}
int main(){
cin >> n;
for(int i = 1;i <= n;i ++){
cin >> weight[i];
}
for(int i = 1;i <= n;i ++){
int tmpf,tmps;
cin >> tmps >> tmpf;
if(tmpf == 0 && tmps == 0)
break;
ind[tmps] ++;
next[tmpf] . push_back(tmps);
}
int root;
for(int i = 1;i <= n;i ++){
if(ind[i] == 0){
root = i;
break;
}
}
// cout << root << " ";
work(root);
cout << max(dp[root][0],dp[root][1]) << endl;
return 0;
}