动态规划

大家好,这里是椋枨(分度值),今天我们就来了解一下动态规划的相关内容(普及组蒟蒻的博客,含金量不高,大家不要嫌弃~)

常见问题

  • 做决策
  • 求最优解
  • 求方案数
  • 求可行解
  • 大多是搜索会炸,贪心不敢做的题

设计状态(阶段)

  • 题目问什么,就没什么
  • 从数据范围推出状态

- 两种思路:

1. 满状态求出的最优解就是答案——最优解问题
2. 状态即为答案,该状态是否能够达到即为该答案是否合法——可行解问题

- 状态必须是可以继续细分的

大问题--->小问题

- 根据状态不同可以分为:

线性DP、区间DP、树形DP、状压DP等。

推导状态转移问题

  1. 思考如何从小问题推导成大问题
  2. 通常需要分情况讨论:考虑最后一步是如何做决策的
  3. 由状态转移方程来决定推导的顺序

- 线性:从左到右、从右到左 -

- 区间:从短到长 -

- 分配/背包:从小背包推到大背包 -

- 树形:自底向上、自上而下 -

- 状态压缩:从0推到满状态、从起始状态推到目标状态 -

  • 若顺序难以确定,可以考虑记忆化搜索。

确定初始状态:

即不需要太多决策、能够立刻得到答案的最小的问题

常见地,

  1. 线性:左端点、右端点、边界外
  2. 区间:长度为0/1/2……
  3. 分配/背包:背包大小为0
  4. 树形:叶子节点、根节点
  5. 状态压缩:0状态或题目给定的起始状态

……
作为状态转移的已知条件

线性DP

区间DP

分配类DP

树状DP

- 题单

实现形式

  • 树形dp的主要实现形式是dfs,在dfs中dp,主要的实现形式是dp[i][j][0/1],i是以i为根的子树,j是表示在以i为根的子树中选择j个子节点,0表示这个节点不选,1表示选择这个节点。有的时候j或0/1这一维可以压掉

基本的dp方程

  • 选择节点类
    {dp[i][0]=dp[j][1]
    {dp[i][1]=max/min(dp[j][0],dp[j][1])

  • 树形背包类
    {dp[v][k]=dp[u][k]+val
    {dp[u][k]=max(dp[u][k],dp[v][k-1])

详细内容戳这里


背包

滚动数组

这里

01背包

完全背包


多重背包+01背包+完全背包

二进制优化(多重背包转01背包)

  1. 做法:把每一个物品根据2的多少次方拆分,因为任何数都可以转化为二进制数

  2. 核心思想:把每一个物品拆成很多个,分别计算价值和所需时间,再转化为01背包求解

  3. 最后一点:完全背包可以把他的空间记为999999,不要太大,一般百万就足够了

  • 将使用次数为c的物品拆分为权值为1, 2, 4, ...2i,c-2(i+1)+1的物品。 比如(w=2,v=3,c=13)可拆分为(2,3),(2∗2,3∗2),(2∗4,3∗4),(2∗6,3∗6),容易证明这log(c)个数的物品可以拼接出[1,c]的所有数,所以是合法的。
#include<cstdio>
#include<algorithm>
#define re register int
using namespace std;
int nx,ny,ex,ey,n,f[1010];
int a[10005],b[10005],c[10005];
int tp,co[1000005],v[1000005];//尽可能开大,不要把空间开爆了
inline void pre() {
	for(re i=1;i<=n;i++) {
		int t=1;
		while(c[i]) {
			co[++tp]=t*a[i];
			v[tp]=t*b[i];
			c[i]-=t; t*=2;
			if(c[i]<t) {//如果剩下的不能再拆,就直接放在一起
				co[++tp]=a[i]*c[i];
				v[tp]=b[i]*c[i];
				break;
			}
		}
	}
}
int main() {
	scanf("%d:%d%d:%d%d",&nx,&ny,&ex,&ey,&n);
	int t=(ex*60+ey)-(nx*60+ny);
	for(re i=1;i<=n;i++) {
		scanf("%d%d%d",&a[i],&b[i],&c[i]);
		if(!c[i]) c[i]=999999;
	}
	pre();//二进制拆分
	for(re i=1;i<=tp;i++) {//考虑每个拆出来的物品
		for(re j=t;j>=co[i];j--)//01背包板子
			f[j]=max(f[j],f[j-co[i]]+v[i]);
	}
	printf("%d",f[t]);
	return 0;
} 

(膜拜sel_fish)

如果还不懂的同学珂以康康这篇博客


单调队列优化 (相比于二进制优化,思维难度略大,效率略高)

不懂单调队列的同学这里不会吧不会吧,不会真的有人不会单调队列?)~

  • 容易发现,在读入(v,w,c)时,f[j]可由 f[j-v], f[j-v * 2],...f[j-v * c]推得。
    而 f[j-v]可以由 f [j-v * 2],...f [j-v * c],f [j-v * (c+1)]推得。

  • 它们的备选区间只在左右端点有变化。

  • 又有f[j + p * v] = max(f[j + k * v]+(p-k) * w) (k<=c)

  • 将右边化简为(f [j + k * v ]-k * w)+p * w,因此我们枚举固定的j作为模数(代码中为 d),在p不断后移的同时,用单调队列维护在范围内的 f[j+k * v ] - k * w最大值即可。由于又要通过式子的值淘汰无用值,又要用下标淘汰过期值,我使用了两个队列。(奇怪,洛谷上我打的乘号怎么显示不了?)

#include <bits/stdc++.h>
#define MAX (1000 + 7)
using namespace std;
int N, M, a0, b0, a1, b1, f[MAX], Q1[MAX], Q2[MAX];
int main()
{
	scanf("%d:%d %d:%d %d", &a0, &b0, &a1, &b1, &N);
	M = (a1 - a0) * 60 + b1 - b0;
	
	for (int i = 1, v, w, c; i <= N; i++)
	{
		scanf("%d%d%d", &v, &w, &c);
		if (!c)
		{
			for (int j = v; j <= M; j++)
				f[j] = max(f[j], f[j - v] + w);
			continue;
		}
		for (int d = 0; d < v; d++)
		{
			int L = 1, R = 0, maxp = (M - d) / v;
			for (int p = 0; p <= maxp; p++)
			{
				int &x = f[d + v*p];
				while (L<=R && x - w*p >= Q2[R]) R--;
				Q1[++R] = p, Q2[R] = x - w*p;
				while (L<=R && Q1[L] < p - c) L++;
				x = max(x, Q2[L] + w * p);
			}
		}
	}printf("%d\n", f[M]);
}

(膜拜Ofnoname)

拓展:卡特兰数

posted @ 2020-10-03 21:59  椋枨  阅读(54)  评论(0编辑  收藏  举报