常见DP优化
倍增优化DP
在线性DP中,我们一般是按照阶段将DP的状态线性增长,但是我们可以运用倍增思想,将线性增长转化为成倍增长
对于应用倍增优化DP,一般分为两个步骤
1.预处理 ,我们使用成倍增长的DP计算出与二的整次幂有关的代表状态
2.拼凑,根据二进制划分的思想,使用预处理出的状态拼凑出最后的答案(注意,此处一般是倒序循环)
好的下面来一道经典题目详细解释:
[NOIP2012 提高组] 开车旅行
题目描述
小
旅行过程中,小
小
在启程之前,小
1、 对于一个给定的
2、对任意给定的
输入格式
第一行包含一个整数
第二行有
第三行包含一个整数
第四行为一个整数
接下来的
输出格式
输出共
第一行包含一个整数
接下来的
【数据范围与约定】
对于
对于
对于
对于
对于
数据保证
分析:我们先预处理出
仔细阅读题面,我们会发现,本题有三个信息:1.天数,2.城市,3.小A和小B分别行驶的路程总长度
于是我们仔细思考可以发现,当我们知道了行驶天数和出发城市之后,我们肯定可以知道小A和小B分别行驶的路径总长度以及终点城市
于是这启发我们使用动态规划:但是观察
具体的,我们设
初值:
由于当
当
此时我们就可以通过递推求出
于是我们设
与F数组相似的,我们有
初值:
当
当
对此进行DP,我们可以在
下面我们考虑询问
记
1.我们按照二进制从大到小枚举2的整次幂,记作
2.若
则令
循环结束后
枚举起点计算就可以解决问题一,问题二就等同于计算多次
#define int long long;
struct node{
int val,f,ch[2],id;
}t[100005]
int root,tot;
#define lc(x) t[x].ch[0];
#define rc(x) t[x].ch[1];
void rotate(int x){
int y=t[x].f,z=t[y].f,k=rc(y)==x;
t[z].ch[rc(z)==y]=x;
t[x].f=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[y].ch[k]].f=y;
t[y].f=x;
t[x].ch[k^1]=y;
}
void splay(int x,int goal=0){
while(t[x].f!=goal){
int y=t[x].f,z=t[y].f;
if(z!=goal){
rc(y)==x^rc(z)==y?rotate(x):rotate(y);
}
rotate(x);
}
if(!goal)root=x;
}
void insert(int id,int val){
int p=0,u=root;
while(u)p=u,u=t[u].ch[t[u].val<val];
u=++tot;
if(!p){
root=u;
t[u].f=0;
}
else{
t[p].ch[t[p].val<val]=u;
t[u].f=p;
}
t[u].ch[0]=t[u].ch[1]=0t[u].id=id,t[u].val=val;
splay(u);
}
int nxt(int val,int p){//0前驱1后继
int u=root;
while(t[u].ch[val>t[u].val]&&t[u].val!=val)u=t[u].ch[t[u].val<val];
splay(u);
u=t[u].ch[p];
while(t[u].ch[!p])u=t[u].ch[!p];
return u;
}
/*---------------以上是平衡树Splay求前驱后继----------------*/
#define N 100005
#define INF 0x7f7f7f7f7f7f
int n,m,h[N],x[N],s[N],ga[N],gb[N],w;
int f[18][N][2],da[18][N][2],db[18][N][2],la,lb;
void calc(int S,int X) {
la=lb=0;
int p=S;
for(int i=w;i>=0;i--)
if(f[i][p][0]&&la+lb+da[i][p][0]+db[i][p][0]<=X) {
la+=da[i][p][0];
lb+=db[i][p][0];
p=f[i][p][0];
}
}
signed main(){
// freopen("P1081_2.in","r",stdin)
scanf("%lld",&n);
insert(0,-INF);
insert(0,INF-1);
insert(0,INF);
insert(0,1-INF);/*插入哨兵方便计算*/
for(int i=1;i<=n;i++){
scanf("%lld",&h[i]);
}
scanf("%lld%lld",&x[0],&m);
for(int i=1i<=mi++)scanf("%lld%lld",&s[i],&x[i]);
w=log(n)/log(2);
for(int i=n;i>0;--i){
insert(i,h[i]);
int hj1=nxt(h[i],1);
int hj2=nxt(t[hj1].val,1);
int qq1=nxt(h[i],0);
int qq2=nxt(t[qq1].val,0);
int a=t[hj1].id==0?INF:t[hj1].val-h[i];
int b=t[qq1].id==0?INF:h[i]-t[qq1].val;//小A次小,小B最小
if(b<=a){
gb[i]=t[qq1].id;
b=t[qq2].id==0?INF:h[i]-t[qq2].val;
ga[i]=b<=a?t[qq2].id:t[hj1].id;//大小关系一定注意
}
else {
gb[i]=t[hj1].id;
a=t[hj2].id==0?INF:t[hj2].val-h[i];
ga[i]=b<=a?t[qq1].id:t[hj2].id;
}
}
for(int i=1;i<=n;i++)f[0][i][0]=ga[i],f[0][i][1]=gb[i];
for(int i=1;i<=n;i++)f[1][i][1]=f[0][f[0][i][1]][0],f[1][i][0]=f[0][f[0][i][0]][1];
for(int i=2;i<w;i++){
for(int j=1;j<=n;j++){
f[i][j][0]=f[i-1][f[i-1][j][0]][0];
f[i][j][1]=f[i-1][f[i-1][j][1]][1];
}
}
for(int i=1;i<=n;i++){
da[0][i][0]=abs(h[i]-h[ga[i]]);
db[0][i][0]=0;
da[0][i][1]=0;
db[0][i][1]=abs(h[i]-h[gb[i]]);
}
for(int i=1;i<=n;i++){
da[1][i][0]=da[0][i][0];
db[1][i][0]=db[0][f[0][i][0]][1];
da[1][i][1]=da[0][f[0][i][1]][0];
db[1][i][1]=db[0][i][1];
}
for(int i=2;i<w;i++){
for(int j=1;j<=n;j++){
da[i][j][0]=da[i-1][j][0]+da[i-1][f[i-1][j][0]][0];
da[i][j][1]=da[i-1][j][1]+da[i-1][f[i-1][j][1]][1];
db[i][j][0]=db[i-1][j][0]+db[i-1][f[i-1][j][0]][0];
db[i][j][1]=db[i-1][j][1]+db[i-1][f[i-1][j][1]][1];
}
}
/*如分析所言DP*/
calc(1,x[0]);
double ans=(lb?(double)la/lb:2e9);
int num=1;
for (int i=2;i<=n;i++) {
calc(i,x[0]);
if ((double)la/lb<ans||(((double)la/lb==ans)&&h[i]>h[num])){
num=i;
ans=(double)la/lb;
}
}
printf("%lld\n",num);
for(int i=1;i<=m;i++){
calc(s[i],x[i]);
printf("%lld %lld\n",la,lb);
}
}
最后,使用倍增优化DP的前提条件是问题的答案具有强可拼接性,即我们划分阶段做出决策的时候可以任意划分不影响答案(对划分有着限制,比如某个阶段不可以拼凑答案就不可以使用),这样我们就可以把答案的计算变成二的整次幂。
多次查询的dp问题:一般数据结构维护,重复计算很多的话可以倍增优化
数据结构优化DP
在DP过程中有着阶段,状态,决策三个步骤,我们之前的倍增DP和状压DP是从阶段设计和状态上入手进行的优化,数据结构优化DP就是从决策方面对DP进行的优化,包括我们下面的单调队列也是如此
思想概述:运用数据结构的功能加速最优决策的寻找与状态的转移
下面附上常用数据结构的功能
数据结构 | 支持操作 | 均摊时间复杂度 | 代码难度 | 常数 | 扩展性 |
---|---|---|---|---|---|
线段树 | 维护区间信息(可加性),区间修改 | 单次 |
一般 | 较大 | 较好 |
树状数组 | 维护前缀和,区间前缀最值,单点修改 | 单次 |
小 | 很小 | 不好 |
平衡树 | 维护最值,前驱后继,删除节点,rank | 单次期望 |
较大 | 较大 | 较好 |
关于序列70%的问题,平衡树操作,区间操作 | 单次期望 |
略大 | 较大 | 好 | |
堆( |
插入,删除(STL不支持,懒惰删除法),最值 | 单次 |
手写较大 | 很小 | 不好 |
分块 | 区间几乎所有问题 | 单次 |
一般 | 较小 | 极好 |
树套树 | 取决于嵌套的结构,一般为功能总和 | 单次 |
极大(用得少) | 大 | 较好 |
(可持久化)trie | 关于异或的操作(区间操作需要可持久化) | 单次 |
较小 | 较小 | 一般 |
可持久化线段树 | 线段树操作(除区间修改),区间rank,历史版本查询 | 单次 |
较小 | 较小 | 一般 |
注:
1.普通线段树需要
2.线段树和树状数组可以经过离散化开值域上的,不仅仅限于序列上的
在状态转移方程中,如遇到某些数据结构支持的操作,便可以使用数据结构进行优化,下面给几个例题感受一下
清理班次2
题目描述:有
由题:设
初值:
于是我们观察状态转移方程发现,我们需要查询一个区间内的最小值,而朴素写法无疑需要
memset(f,0x3f,sizeof f);
int S,T;
scanf("%d%d%d",&n,&S,&T);
if(T>0)build(1,1,T);
if(T>0)f[0]=0;
int sum=0;
for(int i=1;i<=n;i++)scanf("%d%d%d",&ask[i].x,&ask[i].y,&ask[i].z),sum+=ask[i].z;
sort(ask+1,ask+n+1,cmp) ;
for(int i=1;i<=n;i++){
int mn=ask[i].x-1<=S?0:query(1,ask[i].x-1,min(T,ask[i].y-1));
f[ask[i].y]=min(mn+ask[i].z,f[ask[i].y]);
if(ask[i].y>0)update(1,ask[i].y,f[ask[i].y]) ;
}
printf("%d\n",f[T]>sum?-1:f[T]);
赤壁之战
题意:给定一个长度为
由题,很容易想到
设
不难得出状态转移方程:
在这里,我们需要维护的决策集合为二元组
不过由于平衡树的常数较大,且实现难度较大,于是我们采用维护值域的树状数组做法
具体的:我们将
1.对于插入决策的操作,就把
2.对于查询操作,就在树状数组中计算
for(int i=1i<=mi++){
memset(c,0,sizeof c);//树状数组;
add(val(a[0]),f[0][i-1]);
for(int j=1;j<=n;j++){
f[j][i]=ask(val(a[j])-1);
add(val(a[j]),f[j][i-1]);
}
}
估算
给定一个长度为
如果要求数组
题意即为我们将
设
若我们知道
而
总的时间复杂度是
const int N=2050,K=30;
int f[N][K],midc[N][N],cost[N][N],a[N],n,k;
priority_queue<int>p,q;//p大根堆,q小根堆
int main(){
//freopen("estimate.in","r",stdin);
while(1){
n=read(),k=read();
if(n==0&&k==0)return 0;
for(int i=1;i<=n;i++){
a[i]=read();
}
for(int l=1;l<=n;l++){
priority_queue<int> empty1;
priority_queue<int> empty2;
swap(empty1, p);
swap(empty2, q);
int sumA=0,sumB=0;
for(int r=l;r<=n;r++){
if(l==r){
p.push(a[l]);
midc[l][r]=a[l];
sumA+=a[l];
cost[l][l]=0;
continue;
}
int siz=r-l+1;
if(p.empty())p.push(a[r]),sumA+=a[r];
else if(a[r]>p.top())q.push(-a[r]),sumB+=a[r];
else p.push(a[r]),sumA+=a[r];
while(p.size()>q.size()+1){
q.push(-p.top());sumB+=p.top(),sumA-=p.top();p.pop();
}
while(p.size()<q.size()){
sumA+=-q.top();
sumB+=q.top();
p.push(-q.top());q.pop();
}
midc[l][r]=(r-l+1)&1?p.top():p.top()-q.top()>>1;
cost[l][r]=(p.size()-q.size())*midc[l][r]+sumB-sumA;
}
}
memset(f,0x3f,sizeof f);
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=min(k,i);j++){
for(int q=i-1;q>=j-1;--q){
f[i][j]=min(f[q][j-1]+cost[q+1][i],f[i][j]);
}
}
}
printf("%d\n",f[n][k]);
}
return 0;
}
总结:无论DP的决策限制条件有多少,我们都要尽量对其进行分离,多维DP在执行内层循环时,我们可以将外层循环变量看作定值,状态转移取最优决策时,简单的限制条件循环处理,复杂的限制条件用数据结构维护,注重二者配合,在更难的题目中,还会出现数据结构的嵌套,以同时满足更多的限制条件
单调队列优化DP
思想:借助单调性,及时排出不可能的决策(可行性或最优性),保持决策集合的高效性与秩序性
模型:1D/1D动态规划
解释:这是一个最优化问题,其中的
注意:
编写技巧:
1.可以通过一些技巧将原本不可以使用单调队列优化的转换为这个模型
2.模型很可能只是内层的一维,外面还有维度,这时候我们使用的单调队列需要清空
3.注意单调队列与贪心的结合,很多时候是由贪心导出单调性
4.有些时候单调队列只能够维护决策集合,而不能高效的找出答案,这时候我们就需要其他的数据结构与单调队列建立映射关系,以此借另一个数据结构来快速找出答案,注意,单调队列增删,另一个数据结构也必须同步
5.可删堆可以用
6.单调队列解决问题维护一般分为三步:第一步:排出队头过于古老的决策,即超出了范围,第二步:取出队头决策更新状态。第三步:将此次更新的新状态作为以后的决策插入队列,插入时将所有比新决策坏的决策全部扔掉
7.单调队列内部其实是具有两段性的
8.对于如何猜性质,并且以此应用单调队列,我们可以用归纳法从反向思考要满足决策最优或者是某个决策必定不优,也或者演绎法直接硬钢(需要灵感)。当然需要注意的是我们可以先判断某两个决策相比较其中一个一定不优的情况,用数学归纳法一路推演到最优性(类比法)
例题感受
[SCOI2010]股票交易
题目描述
最近
通过一段时间的观察,
另外,股票交易所还制定了两个规定。为了避免大家疯狂交易,股票交易所规定在两次交易(某一天的买入或者卖出均算是一次交易)之间,至少要间隔
在第
对于所有的数据,
注意读题,我们很容易写出DP状态设计
设
那么我们现在有四种策
1.不买也不卖:
2.凭空买
3.从以前基础上买入:
4.从以前基础上卖出
观察方程式,我们会发现操作一,啥也不买
我们发现,将
对其进行变式,得到
观察这个式子,看出来了有木有!这就是我们上面提到的模型,准确的来说,随着
同样的,操作4也可以像这样使用单调队列优化,不过需要注意的是,由于操作4是增加操作,我们需要倒序枚举
于是我们得到了一个
#define max(a,b) (a)>(b)?(a):(b)
int n,m,ap,bp,as,bs,w,ans=0,f[2001][2001],l,r,q[2001];
int main(){
scanf("%d%d%d",&n,&m,&w);
memset(f,0x8f,sizeof(f));
for(int i=1;i<=n;i++){
scanf("%d%d%d%d",&ap,&bp,&as,&bs);
for(int j=0;j<=as;j++)f[i][j]=-j*ap;
for(int j=0;j<=m;j++)f[i][j]=max(f[i][j],f[i-1][j]);
if(i<=w)continue;
l=1,r=0;
for(int j=0;j<=m;j++){
while(l<=r&&q[l] < j-as)l++;
while(l<=r&&f[i-w-1][q[r]]+q[r]*ap<=f[i-w-1][j]+j*ap)r--;
q[++r]=j;
if(l<=r)f[i][j]=max(f[i][j],f[i-w-1][q[l]]+q[l]*ap-j*ap);
}
l=1,r=0;
for(int j=m;j>-1;j--){
while(l<=r&&q[l]>j+bs)l++;
while(l<=r&&f[i-w-1][q[r]]+q[r]*bp<=f[i-w-1][j]+j*bp)r--;
q[++r]=j;
if(l<=r)f[i][j]=max(f[i][j],f[i-w-1][q[l]]+q[l]*bp-j*bp);
}
}
printf("%d\n",f[n][0]);
return 0;
}
裁剪序列
题面:给定一个长度为
试计算这个最小值。
序列
看题目,提取关键信息:1.序列的长度2.每段和不超过
于是我们可以设计出DP的状态:设
则有状态转移方程:
考虑优化:和的处理很明显可以用前缀和,这个式子很容易看出来
这时候我们因为有了单调性,所以可以考虑使用单调队列
下面我们分析两个决策的优劣,设
试比较右式大小,分别是
我们发现,因为
但因为若此式成立,当且仅当不等式取等号,于是乎,运用上文提到的数学归纳法,容易得到:
引理:决策
1.
2.
小总结:在这道题的结论猜测推导中,我们先是将两个决策作比较,这是运用单调队列优化寻找性质的常用手段,然后进行化一般为特殊,找到满足条件的特殊情况,对此运用归纳法进行倒推,得到一定性质之后大胆猜想,从而使用数学归纳法得出结论,在这里,我们其实也可以运用反证法以演绎法的思路去推导得出结论。反正就是列出状态相关不等式,从中找到性质
于是乎,我们有了这两个性质之后,便可以应用单调队列优化了
需要注意的是,对于性质2,我们可以使用双指针
表面上看,一个区间最值,可以直接上这就不用单调队列了
当然,我们为了追求时间的绝对快速,使用单调队列,根据引理,对于一个新决策
代码中采用set
#define ll long long
using namespace std;
int n,a[100010],c[100010],q[100010];
ll m,f[100010];
multiset<ll> s;
int main(){
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
ll sum=0;
for(int i=1,j=0;i<=n;i++){
sum+=a[i];
while(sum>m)sum-=a[j+1],j++;
c[i]=j;
}
int l=1,r=0;
for(int i=1;i<=n;i++){
while(l<=r&&q[l]<=c[i])
s.erase(f[q[l]]+a[q[++l]]);
while(l<=r&&a[q[r]]<=a[i])
s.erase(f[q[r-1]]+a[q[r--]]);
if(l<=r)s.insert(f[q[r]]+a[i]);
q[++r]=i;
f[i]=f[c[i]]+a[q[l]];
if(!s.empty())f[i]=min(f[i],*s.begin());
}
printf("%lld",f[n]);
}
单调队列优化多重背包
多重背包模型:有
我们这样来思考,既然要使用单调队列优化多重背包,那么我们需要寻找决策集合的重叠性
我们循环的时候,在同一个外层
详细的说,我们把倒序循环
运用单调队列进行优化即可
// 单调队列优化多重背包
int n, m, V[210], W[210], C[210];
int f[20010], q[20010];
int calc(int i, int u, int k) {
return f[u + k*V[i]] - k*W[i];
}
int main() {
cin >> n >> m;
memset(f, 0xcf, sizeof(f)); // -INF
f[0] = 0;
// 物品种类
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &V[i], &W[i], &C[i]);
// 除以V[i]的余数
for (int u = 0; u < V[i]; u++) {
// 建立单调队列
int l = 1, r = 0;
// 把最初的候选集合插入队列
int maxp = (m - u) / V[i];
for (int k = maxp - 1; k >= max(maxp - C[i], 0); k--) {
while (l <= r && calc(i, u, q[r]) <= calc(i, u, k)) r--;
q[++r] = k;
}
// 倒序循环每个状态
for (int p = maxp; p >= 0; p--) {
// 排除过时决策
while (l <= r && q[l] > p - 1) l++;
// 取队头进行状态转移
if (l <= r)
f[u + p*V[i]] = max(f[u + p*V[i]], calc(i, u, q[l]) + p*W[i]);
// 插入新决策,同时维护队尾单调性
if (p - C[i] - 1 >= 0) {
while (l <= r && calc(i, u, q[r]) <= calc(i, u, p - C[i] - 1)) r--;
q[++r] = p - C[i] - 1;
}
}
}
}
int ans = 0;
for (int i = 1; i <= m; i++) ans = max(ans, f[i]);
cout << ans << endl;
}
斜率优化DP
这里推荐一个博客
【学习笔记】动态规划—各种 DP 优化
前置知识
平面直角坐标系内,两点连线的斜率:
Ⅰ 状态转移方程
列出状态转移方程,如果化简为以下的形式:
就可以考虑斜率优化DP。
需要注意的是:
大写
此时时间复杂度
先去掉
将只有
Ⅱ 决策点关系
我们可以分析:如果存在
小于或大于
小于或大于
令:
当上述不等式成立时,
注:推式子时记得让
Ⅲ 凸壳
为方便计算,将
仔细观察,上述不等式很像斜率式(所以是字母
假设有三个决策点的
当
如图:
如果不等式符号为
有三种情况:
综上所述,
不难发现,可能成为决策点的点形成了一个下凸壳:
如果不等式符号为
啥也研究不出……
当
思路同上,如图:
如果不等式符号为
同理,但形成上凸壳,不再证明。
总结:取min时使用下凸壳,取max时使用上凸壳
于是当min时我们维护一个斜率单调递增的点集,max时维护一个斜率单调递减的点集
如果不等式符号为
啥也研究不出……
Ⅳ 维护答案
求值
下文默认下凸壳。
假设平面上已经维护了一个凸壳,现在需要知道
用眼睛可以看出五号点就是我们需要的决策点:
设维护出凸包的点集为
由于凸壳的性质(默认下凸),这些点满足:
注意到优劣满足传递性,即如果
因此,可以使用二分,
初始化将
int l = 1,r = m - 1,j = -0x3f3f3f3f
k0 = xxx
如果此时
while(l <= r)
{
ll mid = l + r >> 1;
// >=
if(k0 * (X(mid + 1) - X(mid)) <= (Y(mid + 1) - Y(mid)))
r = mid - 1,j = mid;
else
l = mid + 1;
}
如果二分结束后,
if(j == -0x3f3f3f3f)
j = m;
然后根据转移方程填充
注:维护的数据结构不同,需要适当地改变二分模板。
加点
也就是把一个点(
动态凸包问题可以使用平衡树或CDQ分治解决。
笔者不擅长CDQ,这里介绍平衡树做法:
(本段默认下凸,不会平衡树可跳过此段)
① 判断点是否在凸壳内部
如图所示:如果以该点为观测点,该点的前驱在后继的顺时针方向,那么就说明在凸壳内部(上凸对应逆时针),反之就在外部。
特殊地,没有前驱或后继相当于在外部。
下面是 ``set` 实现的伪代码
struct node
{
int x,y;
node operator - (const node &B)const
{
return (node){x - B.x,y - B.y};
}
int operator * (const node &B)const
{
return x * B.y - y * B.x;
}
bool operator < (const node &B)const
{
return (x != B.x) ? x < B.x : y > B.y;
}
};
multiset<node>s;
#define sit multiset<node>::iterator
顺逆时针叉积判断即可
bool inside(sit p)
{
if(p == s.begin())
return 0;
sit nx = next(p);
if(nx == s.end())
return 0;
sit pre = prev(p);
return ((*pre - *p) * (*nx - *p)) > 0;
// <
}
②加点
很明显,如果该点在凸壳内部,就无需加入凸壳(永远不可能成为决策点)
void ins(node t)
{
sit p = s.insert(t);
if(inside(p))
{
s.erase(p);
return;
}
...
否则先将点加入凸壳,再while判断前驱后继要不要删去
while(p != s.begin() && inside(prev(p)))
s.erase(prev(p));
while(next(p) != s.end() && inside(next(p))
s.erase(next(p));
}
(由于set不好自定义二分,一般使用 Splay或fhq以方便二分。
Ⅴ 特殊性
然而大部分题目都用不着上述算法,因为有一些奇妙的单调性:
如果
先计算出
while(hh < tt && k0 >= K(q[hh],q[hh + 1]))
// <=
hh++;
ll j = q[hh];
dp[i] = /**/;
暴力找到决策点,不同于暴力的是不符合条件的点直接弹出队列。
由于每个节点只会被插入和删除一次,统计答案的时间复杂度加速为
如果
这样使得在平衡树中,每次插入点都是在平衡树尾部,也就是说没有必要使用平衡树了,使用栈即可(结合
由单调性可知,
while(hh < tt && K(q[tt - 1],q[tt]) >= K(q[tt - 1],i))
// <=
tt--;
q[++tt] = i;
把不符合凸性的点直接出队,最后将
在凸包最后加点。
加点的时间复杂度加速为
Ⅵ 模板Code
二分(
#include <cstdio>
#define N 300010
#define ll long long
ll q[N],hh = 1,tt = 1;
inline ll Y(ll x){
return /**/;
}
inline ll X(ll x){
return /**/;
}
int main(){
/*输入*/
for(ll i = 1;i <= n;i++){
ll k0 = /**/;
ll l = hh,r = tt - 1,j = -0x3f3f3f3f;
while(l <= r){
ll mid = l + r >> 1;
// >=
if(k0 * (X(q[mid + 1]) - X(q[mid])) <= (Y(q[mid + 1]) - Y(q[mid])))
r = mid - 1,j = q[mid];
else
l = mid + 1;
}
if(j == -0x3f3f3f3f)
j = q[tt];
dp[i] = /**/;
while(hh < tt && (Y(i) - Y(q[tt])) * (X(q[tt]) - X(q[tt - 1])) <= (Y(q[tt]) - Y(q[tt - 1])) * (X(i) - X(q[tt])))
// >=
tt--;
q[++tt] = i;
}
/*输出*/
return 0;
}
都单调:
#include <cstdio>
#include <iostream>
#include <cstring>
#define ll long long
#define N 50005
using namespace std;
ll n,dp[N],q[N];
inline ll X(ll x){
return /**/;
}
inline ll Y(ll x){
return /**/;
}
inline double K(ll j1,ll j2){
double res = (double)(Y(j2) - Y(j1)) / (double)(X(j2) - X(j1));
return res;
}
int main(){
/*输入*/
ll hh = 1,tt = 1;
for(ll i = 1;i <= n;i++){
ll k0 = /**/;
while(hh < tt && k0 >= K(q[hh],q[hh + 1]))
// <=
hh++;
ll j = q[hh];
dp[i] = /**/;
while(hh < tt && K(q[tt - 1],q[tt]) >= K(q[tt - 1],i))
// <=
tt--;
q[++tt] = i;
}
/*输出*/
return 0;
}
Ⅶ 注意事项
ⅰ 有时将除法写成乘法以保证精度
ⅱ 有时,
ⅲ 注意初值,从
ⅳ 对于一些题目,X(j2) - X(j1) == 0 ? eps : X(j2) - X(j1)
,
ⅴ注意要维护严格凸的凸壳,而不是下面这样(共线)
不然就会WA得莫名其妙
ⅵ 加点和统计答案是两个不同的事件,不是所有题目都统计完就加点
ⅶ 凸包维护有时不止一个,具体问题具体分析
Ⅷ 例题
注:此上内容大部分摘自洛谷日报409
K匿名序列
题意给出一个长度为
分析:由于操作只能够进行减少,于是很容易的我们就可以重新描述这个问题
对一个长度为
很轻松的可以设计出状态,设
设
有状态转移方程:
我们发现,状态转移方程里只含有常数项,关于
去掉
得到:
进一步变式:
我们设
那么状态转移方程可以改写为:
同理,上面的变式也可以写作:
此时:这就是一个直线的点斜式方程,其中
事实上,运用斜率优化的时候我们经常通过定义函数
下面我们来比较三个决策:
就如上面分析的那样,记直线
很明显,当
于是我们可以使用单调队列维护这个决策集合,记这个队列为q
1.检查队头:当队头两个决策的斜率小于等于
2.更新答案
3.入队,注意此处加入决策集合的决策是
int s[500005],a[500005],f[500050],n,m,q[500005],l,r;
int X(int j){
return a[j+1];
}
int B(int i){
return f[i]-s[i];
}
int Y(int j){
return B(j)+j*X(j);
}
int dx(int i,int j){
return X(i)-X(j);
}
int dy(int i,int j){
return Y(i)-Y(j);
}//编程时时刻注意模块化编程
int main(){
//freopen("kas 3-6.in","r",stdin);
int t;
scanf("%d",&t);
while(t--){
memset(f,0x3f,sizeof f);
l=r=1;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
s[i]=s[i-1]+a[i];
}
f[0]=0;
for(int i=1;i<=n;i++){
while(l<r&&dy(q[l+1],q[l])<=i*dx(q[l+1],q[l]))l++;
f[i]=Y(q[l])-i*(X(q[l]))+s[i];
if(i<m+m-1)continue;
int qp=i;
i=i-m+1;
while(l<r&&dx(i,q[r])*dy(q[r],q[r-1])>=dx(q[r],q[r-1])*dy(i,q[r]))r--;
q[++r]=i;
i=qp;
}
printf("%d\n",f[n]);
}
}
最后总结:
对于斜率优化
-
设计出状态,写出状态转移方程
-
当状态转移方程形如
,其中函数 都是多项式, 可能不存在, 是常数,且整个方程不含有关于 或 的高次式,此时可以考虑斜率优化 -
对状态转移方程进行变式,删掉
函数,并将其进行一项,将只与j相关的式子移至左边,其余移至右边,将其进行函数化变式,写成点斜式方程的样子 -
举出3个决策
并思考三个决策将其画在平面直角坐标系中的三决策连线的关系,无非只有两个:上凸或者下凸,在这两种情况中有一种是可以判断 是不可能决策,我们便需要维护这种情况的凸包 -
对函数
的单调性进行分类讨论
·
·
·二者都不单调,此时我们需要使用平衡树动态维护凸包
附赠代码
int find(int i,int k){//k即为我们的答案斜率
if(l==r)return q[l];
int L=l,R=r;
while(L<R){
int mid=(L+R)>>1;
if((__int128)(f[q[mid+1]]-f[q[mid]])<=(__int128)k*(C[q[mid+1]]-C[q[mid]]))L=mid+1;
else R=mid;
}
return q[L];
}
//主函数中
l=r=1;
for(int i=1;i<=n;i++){
int p=find(i,s+T[i]);
f[i]=f[p]-(s+T[i])*C[p]+T[i]*C[i]+s*C[n];
while(l<r&&(f[q[r]]-f[q[r-1]])*(C[i]-C[q[r]])>=(f[i]-f[q[r]])*(C[q[r]]-C[q[r-1]]))r--;
q[++r]=i;
}
·当
四边形不等式优化DP
四边形不等式:
定义:
设二元函数
(注意是
定理:
设二元函数
成立,则
证明:
上下两式相加,有:
以此类推
同理
得证
小知识:叫四边形不等式的原因是这样一个函数构造出的矩阵,左下角加右上角大于等于左上角加右下角
一维线性DP的四边形不等式优化
对于形如
决策单调性定理
对于形如
证明:
移项得:
两式相加得:
此式的含义是,对于
于是我们只需要证明
下面我们来谈对具有决策单调性的一维DP如何优化:
首先,因为具备决策单调性,于是我们的p数组大概长这样(设各个决策为j):
当我们需要·插入一个新的决策的时候,由于决策单调性,
直接修改数组效率过低,我们需要更高效的处理方式
我们发现,对于新加入的决策
详细的说,维护一个单调队列,其内存储一个个三元组
1.取出队尾,判断对于新决策
2.比较决策
3.在
4.将队尾出队,依次插入三元组
值得注意的是,当我们插入了决策
附赠模板一份:
struct node{
ll l,r,k;
}q[100050];
void pop(ll i){
while(head<=tail){
if(q[head].r<=i)head++;//删除无用决策[1,i]
else {
q[head].l=i+1;
break;
}
}
}
ll find(ll i){//二分查找到i的最优决策
ll l=head,r=tail;
while(l<=r){
ll mid = l+r>>1;
if(q[mid].l>i)r=mid-1;
else if(q[mid].r<i)l=mid+1;
else return q[mid].k;
}
}
LL calc(ll i,ll j){//val函数,计算代价
//因题而异
}
void insert(ll i){//插入新决策
ll k=-1;
while(head<=tail){
if(calc(q[tail].l,i)<=calc(q[tail].l,q[tail].k))k=q[tail].l,tail--;//步骤1
else {
if(calc(q[tail].r,q[tail].k)<=calc(q[tail].r,i))break;//步骤2
ll l=q[tail].l,r=q[tail].r;
while(l<r){
ll mid=l+r>>1;
if(calc(mid,q[tail].k)<calc(mid,i))l=mid+1;
else r=mid;
}//步骤3
q[tail].r=l-1,k=l;//步骤4.(1)
break;
}
}
if(k==-1)return ;
q[++tail]={k,n,i};//步骤4.(2)
}
使用四边形不等式优化一维线性DP,可以将复杂度从
二维四边形不等式优化DP
常用于区间DP里形如:
或者:
单调队列优化
对于
单调队列优化的步骤可以简单的描述为:
- 对于队头策略进行范围上的比较,超出范围出队
- 使用队头策略更新答案
- 将新进入决策集合的策略加入队尾,加入之前判断队尾是否劣于新决策,是的话出队
有时题目较复杂,我们可以使用单调队列维护决策集合,再使用其他数据结构(支持插入删除)维护高效的取出最优策略
斜率优化
斜率优化一般步骤上文也有,这里再提一次:
-
设计出状态,写出状态转移方程
-
当状态转移方程形如
,其中函数 都是多项式, 可能不存在, 是常数,且整个方程不含有关于 或 的高次式,此时可以考虑斜率优化 -
对状态转移方程进行变式,删掉
函数,并将其进行一项,将只与j相关的式子移至左边,其余移至右边,将其进行函数化变式,写成点斜式方程的样子 -
举出3个决策
并思考三个决策将其画在平面直角坐标系中的三决策连线的关系,无非只有两个:上凸或者下凸,在这两种情况中有一种是可以判断 是不可能决策,我们便需要维护这种情况的凸包 -
对函数
的单调性进行分类讨论
·
·
·二者都不单调,使用平衡树动态维护凸包
四边形不等式优化
推荐先观察题目范围明确需要进行优化,再对其他优化策略一个个排除,都不行的时候将
其他优化
数据结构优化
观察DP方程式,直接对于决策使用数据结构快速检索出最优决策实现快速转移
倍增优化
一般应用较少,需要保证的是答案满足可拼接性,即保证在拼接答案或者是倍增DP的时候不会出现冲突之类的,很灵活
多次查询的dp问题:一般数据结构维护,重复计算很多的话可以倍增优化
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!