动态规划
大家好,这里是椋枨(分度值),今天我们就来了解一下动态规划的相关内容(普及组蒟蒻的博客,含金量不高,大家不要嫌弃~)
常见问题
- 做决策
- 求最优解
- 求方案数
- 求可行解
- 大多是搜索会炸,贪心不敢做的题
设计状态(阶段)
- 题目问什么,就没什么
- 从数据范围推出状态
- 两种思路:
1. 满状态求出的最优解就是答案——最优解问题
2. 状态即为答案,该状态是否能够达到即为该答案是否合法——可行解问题
- 状态必须是可以继续细分的
大问题--->小问题
- 根据状态不同可以分为:
线性DP、区间DP、树形DP、状压DP等。
推导状态转移问题
- 思考如何从小问题推导成大问题
- 通常需要分情况讨论:考虑最后一步是如何做决策的
- 由状态转移方程来决定推导的顺序
- 线性:从左到右、从右到左 -
- 区间:从短到长 -
- 分配/背包:从小背包推到大背包 -
- 树形:自底向上、自上而下 -
- 状态压缩:从0推到满状态、从起始状态推到目标状态 -
- 若顺序难以确定,可以考虑记忆化搜索。
确定初始状态:
即不需要太多决策、能够立刻得到答案的最小的问题
常见地,
- 线性:左端点、右端点、边界外
- 区间:长度为0/1/2……
- 分配/背包:背包大小为0
- 树形:叶子节点、根节点
- 状态压缩: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背包)
-
做法:把每一个物品根据2的多少次方拆分,因为任何数都可以转化为二进制数
-
核心思想:把每一个物品拆成很多个,分别计算价值和所需时间,再转化为01背包求解
-
最后一点:完全背包可以把他的空间记为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)