动态规划

所有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;
}
  1. 分组背包

一个旅行者有一个最多能装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) 按位或

海贼王之伟大航路 OpenJ_Bailian - 4124

#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;
}

P1896 [SCOI2005]互不侵犯

#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又有一个奇特的特点:多数情况下可以用两种方法实现:递归,拓扑。

P1352 没有上司的舞会

/*
	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;
}
posted @ 2019-07-18 19:44  永远_少年  阅读(218)  评论(0)    收藏  举报