当前位置: 首页 > news >正文

DP 总结(未完成)

关于 DP

1.暴力DP

\(A\).线性 DP

没什么具体的定义,是同时针对一个或几个区间上的 dp。一般就是推出状态转移方程后直接码上。

一个要注意的点就是:分类讨论是这类题主要的难点,分讨可以体现在状态设计上,但更多的是在转移。

\((1)\) 转移中分讨:守望者的逃离、TJOI2007] 线段。

\((2)\) 状态上分讨:找爸爸

\((3)\) 双线程:方格取数


\(B\).背包 DP

这类题目中每个物品都有体积和价值,并且有容量的限制。

\((1)\).\(01\) 背包,完全背包,多重背包

这里可以将 \(01\) 背包、完全背包、多重背包统一为多重背包,其中完全背包中的每个物品的最多可选数量就是 $\lfloor \dfrac { 容积}{体积} \rfloor $。

多重背包的样板题:樱花,以下为主要代码:

for(int i=1;i<=n;i++)
{p[i]=(node){ read(),read(),read() };if(!p[i].c) p[i].c=m/p[i].w;
}
for(int i=1;i<=n;i++)for(int j=m;j>=0;j--)for(int k=0;k<=p[i].c;k++)if(j-k*p[i].w>=0)ckmax(dp[j],dp[j-k*p[i].w]+k*p[i].v);

从以上代码可以看出,多重背包的复杂度较大。一个显然的问题是:对于一个物品,我们重复枚举了等价的选择方案(例如:对于有 \(k\) 个同一物品,选第一个和第二个与选第二个和第三个显然是一样的),那么找到一个可以不重不漏的物品拆分方法就可以大大降低复杂度。常用的方法就是二进制分组,可以见背包 DP - OI Wiki中相应部分。

另一种方法就是单调队列优化。

这里提一下单纯的 \(01\) 背包和完全背包的简化写法:

\(dp[j]\) 表示在前 \(i\) 个物品中用 \(j\) 的容量可以获得的最大总价值,这里省去了关于 \(i\) 的一维,因此:

对于 \(01\) 背包:

for(int i=1;i<=n;i++)for(int j=m;j>=w[i];j--)ckmax(dp[j],dp[j-w[i]]+v[i]);

对于完全背包:

for(int i=1;i<=n;i++)for(int j=w[i];j<=m;j++)ckmax(dp[j],dp[j-w[i]]+v[i]);

可以见到,两者的代码上差别主要在第二重循环正序和倒序上,实际上的差异就是在对第 \(i\) 个物品计算 \(dp[j]\) 时,\(dp[j-w[i]]\) 是否被这个物品更新过,\(01\) 背包通过倒序循环来保证 \(dp[j-w[i]]\) 未被物品 \(i\) 更新,从而保证对于每个容量 \(j\), 每个物品最多只会更新容量 \(j\) 一次,而完全背包则恰恰相反,处理物品 \(i\) 时,\(dp[j-w[i]]\) 被物品 \(i\) 以任意次数更新了。

\((2)\).分组背包

实际上就是树上背包的一个简化的形式,又叫有依赖的背包。

典型题目:金明的预算方案。

\((3)\).背包建模

这大概时背包 DP 的一个重大的应用吧,需要转化为价值和体积。

典型题目:飞扬的小鸟,Cow Exhibition G。


\(C\).区间 DP

从区间转移到区间,最显著的特点就是可以从一个小区间的答案计算出大区间的答案

一般来讲,计算区间 \([l,r]\) 具体的方式有两种:分为两个并列的区间 \([l,k]\)\([k+1,r]\) ,递归到内部区间 \([l+1,r-1]\) 后统计端点。

\((1)\).分为两个区间:石子合并、USACO16OPEN 248 G、能量项链、Polygon。

\((2)\).递归后统计端点价值:Coloring Brackets。

DP 常见套路1

如果题目可以转化为处理一个 \(01\)序列,那么大概率是枚举一个极长 \(0/1\) 段的起始位置,状态数组常为 \(dp[i][0/1]\),表示在位置 \(i\) 结束的极长 \(0/1\) 段,方程常为 \(dp[i]=\max_{i=1}^{j-1} (dp[j]+cost(j+1,i))\)

题目:Game Rooms 、染色。


\(D\).数位 DP

个人认为,这简直是 DP 中最背板的一类(doge),并且总是采用记忆化搜索的方式书写。

典型问法:求在区间 \([l,r]\) 中满足某些条件的数的个数。

首先要转化成前缀和的形式:设 \(ans_{[l,r]}\) 表示区间 \([l,r]\) 中满足条件的数的个数,有 \(ans_{[l,r]}=ans_{[1,r]}-ans_{[1,l-1]}\)

接下来就是求解 \(ans_{[1,x]}\)

求解过程中需要讲数位 DP 的精华之处:前导位压制,用来保证我们找到的数都在区间 \([1,x]\) 中。

这里解释以下”前导位压制“的意思:

考虑一个填数的过程:给定一个数 \(x\),要求填出一个不超过 \(x\) 的数。

例如 \(x=123456789\),假设你已经填了前 \(4\) 位成 \(1234-----\),这时要填第五位,因为不能超过 \(x\),那么你只能在第五位填 \(0、1、2、3、4、5\),但如果填的是 \(1231-----\),那么你就可以在第五位任意填一个 \(0\)\(9\) 的数。我们把与第一种类似的情况称为被前导位压制,与第一种类似的情况称为不被前导位压制。

从这个例子可以看出,填出 \(1231-----\)\(1232-----\)\(1134-----\) 等情况后,因为后面都可以随便填,它们接下来的情况都是一样的,这就可以用记忆化搜索来储存,之后查询到就直接调取答案。

以一道简单的题同类分布为例:

\(1\).需要取出 \(x\) 的每一位,存在 \(num\) 中(注意这里是从低位开始存):

inline void GetNum(int x)
{memset(num,0,sizeof(num));len=0;while(x) num[++len]=x%10,x/=10;
}

\(2\).设状态数组,一定要设置一维处理到第 \(i\) 位,然后你就可以自由添加维度,只要这些维度可以帮助你判断所填的数是否符合条件。在本道题中,状态数组为 \(dp[i][left][mod]\)要记住一点:dp数组所存的一定是未被前导位压制时的答案

\(3\).设计记搜状态,一定要设置表示处理到第 \(i\) 位的一维和表示是否被前导位压制的一维,其他的维度同 dp 数组。对于本题,就是 \(dfs(i,left,mod,limit)\)

\(4\).在 dfs 的过程中,需要找到当前数位可填写的最大值 \(up\),当 \(limit==false\) 即未被前导位压制时,\(up=9\),否则 \(up=num[i]\)

\(5\).dp 数组要初始化为 \(-1\),因为方案数显然可以为 \(0\)

\(6\).只要当前位之前有一位数不是贴着上界填,就不算被前导位压制,\(limit==false\)

其他的就是正常记搜了。

int dfs(int p,int left,int mod,bool limit,int sum,int *t)
{if(p==0) return (left==0) && (mod==0);if(!limit && ~dp[p][left][mod][limit])return dp[p][left][mod][limit];int upper=limit?t[p]:9;int tot=0;for(int x=0;x<=upper;x++){if(x>left) continue;tot+=dfs(p-1,left-x,(mod*10+x)%sum,limit&&x==upper,sum,t);}if(!limit) dp[p][left][mod][limit]=tot;return tot;
}

这题的完整代码,注意函数命名略有不同(这题很早前写的,现在码风已经变了):

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF=0x3f3f3f3f;
const int N=19;
const int M=9*N;inline int read()
{int f=1,x=0; char ch=getchar();while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }while(isdigit(ch))  { x=x*10+ch-'0';    ch=getchar(); }return f*x;
}int L,R,tmp[N+1],len;
int dp[N+1][M+1][M+1][2];
// dp[p][left][mod][limit]:
// 处理到第 p 位(从高到低)
// 剩余需要累加的数字之和为 left
// 当前数对 sum 取模为 mod
// 是否被前导位压制 0/1int calc(int p,int left,int mod,bool limit,int sum,int *t)
{if(p==0) return (left==0) && (mod==0);if(!limit && ~dp[p][left][mod][limit])return dp[p][left][mod][limit];int upper=limit?t[p]:9;int tot=0;for(int x=0;x<=upper;x++){if(x>left) continue;tot+=calc(p-1,left-x,(mod*10+x)%sum,limit&&x==upper,sum,t);}if(!limit) dp[p][left][mod][limit]=tot;return tot;
}void fx(int x,int *t,int &pos)
{while(x){t[++pos]=x%10;t[0]+=t[pos];x/=10;}
}int solve(int x)
{memset(tmp,0,sizeof(tmp));int len=0;fx(x,tmp,len);int total=0;for(int sum=1;sum<=9*len;sum++){memset(dp,-1,sizeof(dp));total+=calc(len,sum,0,true,sum,tmp);}return total;
}signed main()
{L=read(),R=read();printf("%lld\n",solve(R)-solve(L-1));return 0;
}

相似的题目:windy 数、手机号码、数字计数

(-------------------------施工中-------------------------)

http://www.hskmm.com/?act=detail&tid=189

相关文章:

  • Code and Data Relocation in Zephyr
  • 产品经理实战指南:用户需求分析全流程详解(含工具链整合)
  • 模板
  • kylin V11安装mysql8.0
  • 【Kubernetes】 PVC 和 PV
  • Docker镜像
  • idea 允许多运行java示例 idea2022版本
  • ROS2环境配置
  • 2025年第五届电子信息工程与计算机科学国际会议(EIECS 2025)
  • P6477 [NOI Online #2 提高组] 子序列问题 题解
  • iframe 跨域通信实战:可视化编辑器的技术实现
  • windows项目下统计代码行数
  • 。。。
  • ETF 简介
  • 实时流式响应的 SSE 技术实现
  • 2025年艺术、教育和管理国际学术会议(ICAEM 2025)- 第五期
  • CF 1048 Div.2 解题报告
  • reLeetCode 热题 100-1 两数之和-扩展1 unordered_map实现 - MKT
  • 读书笔记:什么是对象表?
  • AI 服务路由策略:如何实现智能负载均衡
  • 在SQL语句中的别名
  • 多维度排序算法在企业级应用中的性能优化
  • 正则表达式在代码解析中的高级应用
  • vue3 项目中优雅的使用 SVG 图标(vite-plugin-svg-icons)
  • 自我介绍+软工5问
  • 车道线检测资料
  • 实现Jenkins不同账号只能看到各自任务的权限
  • 6 个最佳无代码 IT 资产管理工具推荐
  • python开发mcp入门
  • 建造者模式进阶:复杂AI服务的优雅构建