决策单调性学习笔记
目录
一、四边形不等式
定义
中的决策单调性
中的决策单调性
二、二分队列写法
三、分治写法
四、区间 写法
五、相关例题
例1、
例2、
例3、
例4、
例5、
例6、
一、四边形不等式
定义
如果二元函数 满足, ,均有 ,则称 满足四边形不等式。
结论:二元函数 满足四边形不等式的充要条件为, ,均有 。
证明:
充分性显然,下面考虑必要性。
条件等价于,对于任意一个 的正方形,左上加右下右上加左下。
目标等价于,对于任意两行两列的子矩阵,左上加右下 右上加左下。
将以 为左上角的正方形的式子相加,可得,对于所有行以及 两列构成的子矩阵,它满足四边形不等式。
在行的维度上做相同的操作即可得证。
中的决策单调性
对于形如 的转移方程,如果最优决策点 (非严格)单调递增,则称 满足决策单调性。
注:如果有多个决策点同时达到最优,我们需要人为规定取最左或最右的决策点,但不能任取一个作为最优决策点。
结论:如果 满足四边形不等式,则 满足决策单调性。
证明:
反证法,假设存在 ,根据最优性:
相加可得 。
注意到 ,但这与四边形不等式矛盾。
如果 的转移是 形式,那么四边形不等式需要反号。总而言之,四边形不等式可以简记为 "交叉优于包含" 。
划重点:如果仅有最优决策满足决策单调性,那么并不能使用下面 "二分队列" 的方法进行优化;我们需要保证对于任意两个决策点,一个前缀更优,一个后缀更优,才可以使用 "二分队列" 进行优化。
中的决策单调性
对于形如 的转移方程,如果最优决策点 满足,则称 满足决策单调性。
结论:如果 满足下述条件,则 满足四边形不等式和决策单调性。
- 满足四边形不等式。
- 。
- 。
证明过于繁琐,懒得写了。甩个链接,想看的自己去看。
由于某些显而易见的原因,本文例题部分不会严谨证明转移矩阵满足四边形不等式。
在实际操作中,如果你认为某个 方程可以用决策单调性优化,不妨通过打表判断决策点是否单调来验证你的猜想。
二、二分队列写法
适用范围:绝大多数 决策单调性优化。
转移方程:,其中 满足四边形不等式。
二分队列的主要思想是对每个 ,维护其最优决策 的值。
每计算出一个 ,它可以作为决策点去更新后面的 值。根据决策单调性, 仅可能作为一个后缀的最优决策(注意当前已有的决策点只有 )。
具体实现时,用一个单调队列维护三元组 {x,l,r}
,表示决策点 是区间 的最优决策点。
在插入一个点 时,倒序枚举每个三元组,如果对于左端点 ,满足最优决策点不再是原来的决策点 (而变为当前点 ),根据决策单调性,整个区间的最优决策点都会变成 ,从而可以把这个区间删去。
如果对于左端点 ,最优决策点并没有发生变化,由于需要变成i
的位置是一段后缀,在区间内二分即可。
时间复杂度。
struct node
{
int x,l,r;
}q[maxn];///单调队列
int calc(int j,int i)
{///j向i的转移代价,保证j<i
return f[j]+w(j,i);
}
int main()
{
q[h=t=1]={0,1,n};///初始所有位置的最优决策点均为0
for(int i=1;i<=n;i++)
{
while(h<t&&q[h].r<i) h++;///删掉已经过时的决策点,注意一定不会删空
f[i]=calc(q[h].x,i);///更新f[i]的值
while(h<t&&calc(i,q[t].l)<=calc(q[t].x,q[t].l)) t--;///如果从i转移比从原先的决策点优,删掉这个区间
int l=max(i,q[t].l-1),r=q[t].r+1;///二分有效区间(l,r],如果i对整个区间都不优返回q[t].r+1
///注意calc函数在j>=i时可能无定义,所以需要保证l>=i
while(r-l>1)
{
int mid=(l+r)/2;
if(calc(i,mid)<=calc(q[t].x,mid)) r=mid;
else l=mid;
}
if(r<=n) q[t].r=r-1,q[++t]={i,r,n};///更新队尾
}
}
注意第16
行pop
时并没有把队列弹空,而是留下最后一个元素用于二分。
:对于另一类满足 的决策单调性,可以用 "二分栈" 实现,具体可见例题柠檬。
三、分治写法
适用范围: 的转移不依赖于 ,常见于分层的 转移。
转移方程: 。
向下递归分治,假设当前正在处理 的值,已知 。
记 ,根据决策单调性, 。
先暴力扫描区间 ,求出 和 的值,再分别递归两侧即可。
每向下递归一层区间 长度会减半,因此至多只会递归 层。
每层 ,时间复杂度为。
int calc(int j,int i)
{///上一层j向当前层i的转移代价,不一定需要j<i
return g[j]+w(j,i);
}
void solve(int l,int r,int L,int R)
{///正在计算f[l~r],保证最优决策点包含[L,R]
if(l>r) return ;
int mid=(l+r)/2,pos=0,val=inf;
for(int i=L;i<=R;i++)
if(calc(mid,i)<val)
pos=i,val=calc(mid,i);
f[mid]=val;///暴力求出f[mid]的值,最优决策点为pos
solve(l,mid-1,L,pos);
solve(mid+1,r,pos,R);
}
如果保证 ,也可以用 "二分队列" 实现。
如果 时 calc
函数无定义,需要返回 inf
。当分治递归到 r<=i
的区间时 f[mid]=inf,pos=0
,但分治可以正常进行,无需特殊处理。
小技巧:如果 不能 计算,但是可以 从 和 递推得到,我们可以用类似莫队的方式移动指针处理。
证明:
对于右端点,单次移动次数和 同阶。
对于左端点,单次移动次数和 同阶。
时间复杂度 。
四、区间 写法
适用范围:一般用于优化 型 。
转移方程: 。
记录最优决策点 ,暴力枚举 范围内的所有决策点即可。
时间复杂度 ,证明如下:
计算 的答案时,枚举量为 。
放在二维矩阵上看,相当于 给下边的格子贡献系数 ,给左边的格子贡献系数 ,总代价为所有格子的系数乘以 之和。
![]()
如上图,灰色是不合法的格子,有且仅有红色格子系数为
+1
,绿色格子系数为-1
。时间复杂度为 。
代码非常好写:
for(int i=1;i<=n;i++) f[i][i]=w(i,i),p[i][i]=i;
for(int len=2;len<=n;len++)///区间长度从2开始枚举
for(int l=1,r=len;r<=n;l++,r++)
{
f[l][r]=inf;
for(int k=p[l][r-1];k<=p[l+1][r];k++)
if(f[l][k]+f[k+1][r]+w(l,r)<=f[l][r])
f[l][r]=f[l][k]+f[k+1][r]+w(l,r),p[l][r]=k;
}
五、相关例题
例1、
题目描述
组数据,给定 个句子,行标准长度为 。每一行可以放若干个连续的句子,相邻两个句子之间需要一个空格。
每一行的不协调度为该行实际长度与 之差的绝对值的 次方,求所有行的不协调度的最小值。
数据范围
- 。
- 句子长度 。
时间限制 ,空间限制 。
分析
记句子长度前缀和为 ,写下前 个句子的最小不协调度为 ,转移方程为:
打表可知,满足四边形不等式,用二分栈实现。
注意本题答案会爆
long long
,所以需要用long double
存储 数组,通过牺牲精度的方式换取更大的值域。
时间复杂度 。
#include<bits/stdc++.h>
#define ld long double
using namespace std;
const int maxn=1e5+5;
int h,l,n,p,t,cas;
char ch[maxn][35];
int s[maxn],pos[maxn];
ld f[maxn];
struct node
{
int x,l,r;
}q[maxn];
ld qpow(ld a,int k)
{
ld res=1;
for(int i=1;i<=k;i++) res*=a;
return res;
}
ld calc(int j,int i)
{
return f[j]+qpow(abs(s[i]-s[j]+i-j-1-l),p);
}
void print(int n)
{
if(!n) return ;
print(pos[n]);
for(int i=pos[n]+1;i<=n;i++) printf("%s%c",ch[i]+1,i!=n?' ':'\n');
}
int main()
{
scanf("%d",&cas);
while(cas--)
{
scanf("%d%d%d",&n,&l,&p);
for(int i=1;i<=n;i++)
{
scanf("%s",ch[i]+1);
s[i]=s[i-1]+strlen(ch[i]+1);
}
q[h=t=1]={0,1,n};
for(int i=1;i<=n;i++)
{
while(h<t&&q[h].r<i) h++;
f[i]=calc(q[h].x,i),pos[i]=q[h].x;///记录最优决策点,用于输出方案
while(h<t&&calc(i,q[t].l)<calc(q[t].x,q[t].l)) t--;
int l=q[t].l-1,r=q[t].r+1;
while(r-l>1)
{
int mid=(l+r)/2;
if(calc(i,mid)<calc(q[t].x,mid)) r=mid;
else l=mid;
}
if(r<=n) q[t].r=r-1,q[++t]={i,r,n};
}
if(f[n]<=1e18) printf("%.0Lf\n",f[n]),print(n);
else printf("Too hard to arrange\n");
printf("--------------------\n");
}
return 0;
}
例2、
题目描述
有 种珠宝,第 种的价格为 元,吸引力为 。
对 ,假如你可以支付不超过 元,求吸引力之和的最大值。
数据范围
- 。
时间限制 ,空间限制 。
分析
普通背包的时间复杂度为 ,肯定过不了。
但是单个物品的价格很小,这启示我们重新设计状态。
将所有价格相同的物品按吸引力降序排序,显然只会取一个前缀,记前缀和为 。
表示考虑所有价格 的物品,总价格为 ,吸引力之和的最大值。转移方程为:
下标按照 分类,转移矩阵 ,满足决策单调性。
关于本题 的决策单调性的感性理解: "交叉" 和 "包含" 选择的物品个数是相同的,但 "交叉" 取的物品吸引力更大,所以更优。
对每个 的剩余系分别用决策单调性优化即可。
时间复杂度 。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e4+5;
int c,k,n,v;
ll f[maxn],g[maxn],s[maxn],dp[maxn];
vector<int> vec[maxn];
void solve(int l,int r,int L,int R)
{
if(l>r) return ;
int mid=(l+r)/2,pos=0;
ll val=-1e18;
for(int i=L;i<=R&&i<=mid;i++)///i>mid时不能转移,可以认为w(i,mid)=-inf
if(g[i]+s[mid-i]>val)
val=g[i]+s[mid-i],pos=i;
f[mid]=val;
solve(l,mid-1,L,pos);
solve(mid+1,r,pos,R);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d%d",&c,&v),vec[c].push_back(v);
for(int i=1;i<=300;i++)
{
if(vec[i].empty()) continue;
sort(vec[i].begin(),vec[i].end(),greater<int>());
int l=vec[i].size();
for(int j=1;j<=k/i;j++) s[j]=j<=l?s[j-1]+vec[i][j-1]:0;
for(int r=0;r<i;r++)
{
int m=0;
for(int j=r;j<=k;j+=i) g[++m]=dp[j];
solve(1,m,1,m);
for(int j=r;j<=k;j+=i) dp[j]=f[j/i+1];
}
}
for(int i=1;i<=k;i++) printf("%lld ",dp[i]);
putchar('\n');
return 0;
}
例3、
同类题。
题目描述
给定长为 的序列 ,要求分成 段,每段代价为 。
求所有划分方案中,代价之和的最小值。
数据范围
- 。
时间限制 ,空间限制 。
分析
表示将 划分为 段,代价之和的最小值。转移方程为:
其中 为区间 中相等元素的对数,满足四边形不等式。
注意 不能 计算,需要用莫队维护指针的方式处理。
时间复杂度 。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5,inf=1e18;
int k,l=1,n,r,sum;
int a[maxn],cnt[maxn];
int f[25][maxn];
void add(int x)
{
sum+=cnt[a[x]]++;
}
void del(int x)
{
sum-=--cnt[a[x]];
}
int calc(int L,int R)
{
while(l>L) add(--l);
while(r<R) add(++r);
while(l<L) del(l++);
while(r>R) del(r--);
return sum;
}
void solve(int i,int l,int r,int L,int R)
{
if(l>r) return ;
int mid=(l+r)/2,pos=0,val=inf;
for(int j=L;j<=R&&j<mid;j++)
if(f[i-1][j]+calc(j+1,mid)<=val)
val=f[i-1][j]+calc(j+1,mid),pos=j;
f[i][mid]=val;
solve(i,l,mid-1,L,pos);
solve(i,mid+1,r,pos,R);
}
signed main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=0;i<=k;i++) for(int j=0;j<=n;j++) f[i][j]=inf;
f[0][0]=0;
for(int i=1;i<=k;i++) solve(i,0,n,0,n);
printf("%lld\n",f[k][n]);
return 0;
}
注意执行到第 行时 pos
可能为零,这意味着 f[i][mid]
是一个不合法的状态( mid<i
)。
此时不需要手动 return
,因为区间内其他f
值可能还没被算到,但是更稳妥的方法是在第 行赋初始值 pos=L
(或 内任何一个数)。
例4、
题目描述
定义 。
组数据,给定 ,求。
数据范围
- 。
时间限制 ,空间限制 。
分析
容易想到动态规划, 表示将 划分成 段,代价之和的最小值。
乍一看状态数 ,但是注意到 ,并且 ,因此当 时,答案一定为 。
于是第一维上界变成 ,总状态数 可以接受。
转移方程如下:
先把 化简一下:
预处理 的前缀和,单次计算可以用整除分块优化到 。
发现 满足四边形不等式。在分治暴力寻找决策点时,由于固定,所以可以从 直接递推得到 的值。
求出整个 数组后可以 回答询问,时间复杂度 。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5,inf=1e18;
int k,n,t;
int b[maxn],p[maxn],s[maxn],phi[maxn];
int f[18][maxn];
int calc(int x,int y)
{
int res=0;
for(int l=x,r=0;l<=y;l=r+1) r=y/(y/l),res+=(r-l+1)*s[y/l];
return res;
}
void solve(int i,int l,int r,int L,int R)
{
if(l>r) return ;
int mid=(l+r)/2,pos=0,val=inf;
for(int j=min(mid,R),now=calc(j+1,mid);j>=L;j--)
{
if(f[i-1][j]+now<val) val=f[i-1][j]+now,pos=j;
now+=s[mid/j];
}
f[i][mid]=val;
solve(i,l,mid-1,L,pos);
solve(i,mid+1,r,pos,R);
}
void init(int n)
{
phi[1]=1;
for(int i=2,cnt=0;i<=n;i++)
{
if(!b[i]) p[++cnt]=i,phi[i]=i-1;
for(int j=1;j<=cnt&&i*p[j]<=n;j++)
{
b[i*p[j]]=1;
if(i%p[j]==0)
{
phi[i*p[j]]=phi[i]*p[j];
break;
}
phi[i*p[j]]=phi[i]*phi[p[j]];
}
}
for(int i=1;i<=n;i++) s[i]=s[i-1]+phi[i];
for(int i=1;i<=n;i++) f[1][i]=i*(i+1)/2;
for(int i=2;i<=17;i++) solve(i,1,n,1,n);
}
signed main()
{
scanf("%lld",&t),init(maxn-5);
while(t--)
{
scanf("%lld%lld",&n,&k);
printf("%lld\n",k>=18?n:f[k][n]);
}
return 0;
}
例5、
题目描述
数轴上有 个点,第 个点的权值为 。
给定起点 ,每次操作要么移动到相邻点,要么停留在第 个点并获得 的收益,每个点至多停留一次。
求 次操作后收益的最大值。
数据范围
- 。
时间限制 ,空间限制 。
分析
假设我们已经确定了经过的区间 ,显然我们只会选择区间内最大的 个点。
用可持久化线段树维护,可以做到单次 计算一个区间 的答案。
对固定的 ,记最优的决策点 为 ,容易发现 单调递增。
分治优化,时间复杂度 。
本题 的空间是过不去的,需要对 离散化。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5,inf=1e9;
int d,n,s,cnt,tot;
ll res;
int a[maxn],c[maxn],rt[maxn];
struct node
{
int ls,rs;
ll cnt,sum;
}f[20*maxn];
void pushup(int p)
{
f[p].cnt=f[f[p].ls].cnt+f[f[p].rs].cnt;
f[p].sum=f[f[p].ls].sum+f[f[p].rs].sum;
}
int insert(int p,int l,int r,int pos)
{
int q=++tot;
f[q]=f[p];
if(l==r)
{
f[q].cnt++,f[q].sum+=c[pos];
return q;
}
int mid=(l+r)/2;
if(pos<=mid) f[q].ls=insert(f[q].ls,l,mid,pos);
else f[q].rs=insert(f[q].rs,mid+1,r,pos);
return pushup(q),q;
}
ll query(int p,int q,int l,int r,int k)
{
if(l==r) return 1ll*c[l]*k;
int mid=(l+r)/2,cur=f[f[q].rs].cnt-f[f[p].rs].cnt;
if(k<=cur) return query(f[p].rs,f[q].rs,mid+1,r,k);
else return f[f[q].rs].sum-f[f[p].rs].sum+query(f[p].ls,f[q].ls,l,mid,k-cur);
}
ll calc(int l,int r)
{
int w=d-(r-l)-min(s-l,r-s);
return w<=0?0:query(rt[l-1],rt[r],1,cnt,min(w,r-l+1));
}
void solve(int l,int r,int L,int R)
{
if(l>r) return ;
int mid=(l+r)/2,pos=0;
ll val=-inf;
for(int i=L;i<=R;i++)
{
ll now=calc(mid,i);
if(now>val) val=now,pos=i;
}
res=max(res,val);
solve(l,mid-1,L,pos);
solve(mid+1,r,pos,R);
}
int main()
{
scanf("%d%d%d",&n,&s,&d),s++;
for(int i=1;i<=n;i++) scanf("%d",&a[i]),c[i]=a[i];
sort(c+1,c+n+1);
cnt=unique(c+1,c+n+1)-c-1;
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(c+1,c+cnt+1,a[i])-c;
rt[i]=insert(rt[i-1],1,cnt,a[i]);
}
solve(1,s,s,n);
printf("%lld\n",res);
return 0;
}
例6、
题目描述
数轴上有 个点 ,你需要在数轴上选 个点 ,使得 最小。
数据范围
- 。
时间限制 ,空间限制 。
分析
显然每个 会作用于一段区间,反过来,对于一个给定的区间 ,将 放在中位数位置一定最优。
表示用 个点覆盖 的最小代价。转移方程为:
其中 表示选择一个 覆盖区间 的代价,可以 递推,也可以求前缀和后单次 回答。
容易发现 满足四边形不等式,用区间 写法优化即可。
如果把第二维放在前面,也可以用二分队列或者分治进行优化,但时间复杂度会多一只 。
边界 ,由于计算 需要用到 和 的值,所以需要先升序枚举 ,再倒序枚举 。
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=3005;
int k,n;
int a[maxn],w[maxn][maxn];
int f[maxn][305],p[maxn][305];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int len=1;len<=n;len++)
for(int l=1,r=len;l<=n;l++,r++)
w[l][r]=w[l+1][r-1]+a[r]-a[l];
memset(f,0x3f,sizeof(f));
f[0][0]=0;
for(int j=1;j<=k;j++)
{
p[n+1][j]=n;
for(int i=n;i>=1;i--)
{
for(int l=p[i][j-1];l<=p[i+1][j];l++)
if(f[l][j-1]+w[l+1][i]<=f[i][j])
f[i][j]=f[l][j-1]+w[l+1][i],p[i][j]=l;
}
}
printf("%d\n",f[n][k]);
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17133297.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具