【9】决策单调性学习笔记
前言
平衡树的笔记好像一直搁置着,但是这个内容实在是太重要了,平衡树就搁着吧。
决策单调性
在动态规划时,若转移时对于一个状态是单点对单点转移,且令 表示状态 的最优决策点,有 有 ,则称这个动态规划的过程具有决策单调性。
决策单调性的判定方式有两种,一种是打表,使用朴素 DP 求出每个点的最优决策点,对于每一个点的最优决策点进行比较,即可猜测是否满足决策单调性。
一般地,生成的一组较大的数据(如 )中若经过打表验证满足决策单调性,那么就大概率满足决策单调性。也许这就是生物中说的不完全归纳法。之后配合感性理解,就能大致理解这题的决策单调性。
另一种方法是四边形不等式法则与区间包含单调性。一般情况下,决策单调性和四边形不等式、区间包含单调性存在极强的联系,它们之间的联系会在下面的动态规划模型中指出。现在,先让我们了解什么是四边形不等式和区间包含单调性。
对于函数 ,,有如下式子:
则我们称函数 满足四边形不等式。这个式子可以记忆为交叉小于等于包含。
如果对于函数 ,如果 ,有 ,则称函数 满足区间包含单调性。
为了方便证明,这里记录几条四边形不等式的性质。摘录自 OI Wiki。
性质 :若函数 和 均满足四边形不等式(或区间包含单调性),则对于任意 ,函数 也满足四边形不等式(或区间包含单调性)。
性质 :若存在函数 和 使得 ,则函数 满足四边形恒等式。当函数 和 单调增加时,函数 还满足区间包含单调性。
性质 :设 是一个单调增加的凸函数,若函数 满足四边形不等式并且对区间包含关系具有单调性,则复合函数 也满足四边形不等式和区间包含单调性。
性质 :设 是一个凸函数,若函数 满足四边形恒等式并且对区间包含关系具有单调性,则复合函数 也满足四边形不等式。
一般地,对于存在决策单调性的题目,我们可以通过分治或者单调队列将复杂度降低一个 并只增加一个 ,是非常优秀的优化方法。
1D/1D 动态规划
xD/yD 动态规划的含义:这个动态规划状态数为 ,转移 ,总时间复杂度 。
对于 1D/1D 动态规划,使用决策单调性可以将复杂度从 优化至 。这种题目一般是这种形式:
如果这个式子中 满足四边形不等式,则整个动态规划的过程具有决策单调性。下面给出证明。
假设存在 且 。因为 是 的最优转移点,则对于其余任意转移点的结果都大于等于 ,则有:
又因为 ,由四边形不等式性质得到:
将上述两式相加,消去左右两边的公共项得到:
则对于 来说, 是一个更优秀的转移点,矛盾。
故 且 ,动态规划满足决策单调性。
注意上述推导是建立在取 的情况下的,如果是取 ,把不等号颠倒一下即可。
发现决策单调性之后,我们有两种方式进行运用。如果状态见转移有依赖,我们使用二分单调队列解决。
具体的,我们认为每一个状态都有一个决策表,存储的的当前的最优决策。当一个状态 转移完成之后,我们可以找到一个最小的位置,使这个位置由 转移比目前决策表的转移更优。那么根据决策单调性,对于这个位置之后的位置,其决策表都更新为 。
我们发现这么做会有很多相同的段,我们把这些段形容成一个三元组 ,表示 的状态最优转移点为 。这些三元组把整个决策表分成了若干个连续的段。
对于 进行转移时,我们先使队头 的三元组出队。显然之后的转移不会用到这些元素。
出队完成之后,因为三元组把整个决策表分成了若干个连续的段,所以如果 ,则上一个出队的元素必然有 ,使 ,否则 一定取不到。因此 ,又因为出队完成之后队首的元素一定满足 ,所以可以之接按照队首的 值作为 的最优决策点转移。
当我们加入一个元素之后,考虑更新决策表。由于单调性,靠后的三元组是容易被更新的。我们从队尾开始反向遍历队列,如果从 转移比从当前三元组的 的转移更优,那么就把这个三元组出队,因为这个三元组不优,没有存的必要,之后由最优转移点为 的三元组包含。具体地,由于决策单调性,我们只需要比较三元组中第一个位置,也就是 即可。
注意上述操作完成之后可能会存在一个三元组只有一部分被更新。记 为队尾三元组的 ,我们通过二分查找 求出以 为最优决策点的最小位置,每次比较从 转移是否比从目前队尾的 转移更优。如果更优就二分更小的位置,更劣就二分更大的位置。
其实这里应该可以二分 ,但是在代码中可能会出现一些奇怪的边界问题,所以我选择了二分 。
正确性是显然的,由于决策单调性,以 为最优决策点的最小位置之后从 转移一定更优,满足二分查找的要求。由于决策单调性,队尾三元组之前的三元组必然不以 为最优决策点。因此,我们一定可以找到以 为最优决策点的最小位置 。
之后,我们把队尾的三元组按照以 为最优决策点的最小位置分开,把原先队尾的三元组的范围变成 ,并压入三元组 把之前出队以及队尾的三元组分开后最优决策点为 的部分包含,就维护完了。
注意如果不存在以 为最优决策点的最小位置,即 ,证明 不为任何状态的最优决策点,根部不需要压入。此时也不会有任何三元组被弹出。
代码可以在例题中查看。注意最开始时决策表全是初始状态,需要压入一个覆盖所有状态的最优决策点为初始状态的三元组。
如果状态见转移没有依赖,我们使用分治解决。在例题 中会讲解这种算法。
例题 :
记 表示第 句话的长度, 为 的前缀和。设状态 表示在第 个位置后换行的最小不协调度,根据题意不难推出如下转移方程:
初始 。这是一个 1D/1D 动态规划,经过打表发现动态规划的过程中具有决策单调性。然后就是决策单调性的模板题,实现上述算法即可,不多赘述。
感性理解这个决策单调性的话就是根据常识高次式一般都满足尽量小的底数之后比较小,如果比较大增长速度会很快,远超减小的速度。理性证明的话根据概念证明,把四边形不等式式子化成函数形式之后根据单调性求解,注意到绝对值进行大力分类讨论,结合奇偶性以及恒等式等知识即可证明。
注意本题大于 的状态不能直接设成 ,这可能会破坏决策单调性。因此,我们存下来每个点的真实值。由于这个值可能会非常大,甚至 long long
都存不下,考虑使用 long double
。虽然 long double
会有一定的精度损失,但是在本题中由于数字不太大可以忽略。
另外代码中 lf=max(1ll,ls[r])
可以直接写为 lf=ls[r]
,二分 和二分 其实区别不大。毕竟初始状态(相当于状态 )的最优转移点不会是任何一个后面的状态,不影响答案。
#include <bits/stdc++.h>
using namespace std;
long long t,n,h,p,a[200000],s[200000],pr[200000],q[200000],ls[200000],rs[200000],b[200000],l=1,r=0;
long double f[200000];
const long double inf=1000000000000000001ll;
char str[100001][40];
long double power(long long a,long long p)
{
long double ans=1,x=a;
while(p)
{
if(p&1)ans=ans*x;
p>>=1;
x=x*x;
}
return ans;
}
long double cal(long long i,long long j)
{
return f[i]+power(abs(s[j]-s[i]+j-i-1-h),p);
}
int main()
{
scanf("%lld",&t);
while(t--)
{
scanf("%lld%lld%lld",&n,&h,&p);
for(int i=1;i<=n;i++)scanf("%s",str[i]+1),a[i]=strlen(str[i]+1),s[i]=s[i-1]+a[i];
l=1,r=0,q[++r]=0,ls[r]=1,rs[r]=n;
for(int i=1;i<=n;i++)
{
while(rs[l]<i&&l<=r)l++;
f[i]=cal(q[l],i),pr[i]=q[l];
while(cal(i,ls[r])<=cal(q[r],ls[r])&&l<=r)r--;
long long lf=max(1ll,ls[r]),rf=rs[r],ans=rs[r]+1;
while(lf<=rf)
{
long long mid=(lf+rf)>>1;
if(cal(i,mid)<=cal(q[r],mid))ans=min(ans,mid),rf=mid-1;
else lf=mid+1;
}
if(ans==rs[r]+1)continue;
rs[r]=ans-1;
q[++r]=i,ls[r]=ans,rs[r]=n;
}
if(f[n]>=inf)printf("Too hard to arrange\n");
else
{
printf("%.0Lf\n",f[n]);
memset(b,0,sizeof(b));
long long x=n;
while(x>0)b[x]=1,x=pr[x];
b[1]=0;
for(int i=1;i<=n;i++)
if(b[i])printf("%s\n",str[i]+1);
else printf("%s ",str[i]+1);
if(n==1)printf("\n");
}
printf("--------------------\n");
}
return 0;
}
2D/1D 动态规划
区间合并
对于 2D/1D 动态规划,使用决策单调性可以将复杂度从 优化至 。这种题目一般是这种形式:
这种类型的 2D/1D 动态规划满足决策单调性的要求为 要同时满足区间包含单调性和四边形不等式。对于这一类动态规划,决策单调性指如下式子:(记 的最优转移点为 )
因此,在第三层枚举转移时,我们可以限制 的枚举范围。可以证明这样做复杂度是 的。
同样的,上述结论是建立在取 的情况下的,如果是取 ,把不等号颠倒一下即可。
例题 :
P1880 [NOI1995] 石子合并 (请使用 的算法解决)
的区间 DP 可以见 【5】区间类型动态规划学习笔记,这里不多赘述。
最小值的转移是满足决策单调性的,我们简单证明一下。
四边形不等式:我们不难发现 ,其中 是数组 的前缀和。因此,对于 ,我们有:
我们发现上述式子是等于 的,所以对于 ,有 ,即满足 。
因此,函数 满足四边形不等式。
区间包含单调性:对于 ,我们有:
因为 为非负数,且 ,所以有 且 ,所以有:
因此,函数 满足区间包含单调性。
综上所述,该动态规划的过程中具有决策单调性。
注意到最大值不一定满足决策单调性,但是我们不妨贪心优化一下,发现最大值只有在分割出一个单独的区间和一个目前区间长度减一的区间时会取到。这是显然的,相当于每一次合并的时候都使用了能使用的最大贡献。因此只需要枚举左右端点即可。时间复杂度还是 。
代码是在古早码风基础上改的,码风有点抽象。
#include <bits/stdc++.h>
using namespace std;
int n,min1[600][600],max1[600][600],pr[600][600],a[20000],mina=99999999,maxa=0;
int main()
{
scanf("%lld",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
a[n+i]=a[i];
}
for(int i=1;i<n*2;i++)
pr[i][i]=i,a[i]+=a[i-1];
for(int i=0;i<n*2;i++)
for(int j=0;j<n*2;j++)
if(i!=j)min1[i][j]=99999999;
for(int i=n*2-1;i>=0;i--)
for(int j=i+1;j<=2*n-1;j++)
for(int k=max(pr[i][j-1],i+1);k<=min(pr[i+1][j],j);k++)
if(min1[i][k-1]+min1[k][j]+a[j]-a[i-1]<min1[i][j])min1[i][j]=min1[i][k-1]+min1[k][j]+a[j]-a[i-1],pr[i][j]=k;
for(int i=n*2-1;i>=0;i--)
for(int j=i+1;j<=2*n-1;j++)
max1[i][j]=max(max1[i][i]+max1[i+1][j]+a[j]-a[i-1],max1[i][j-1]+max1[j][j]+a[j]-a[i-1]);
for(int i=0;i<n;i++)
{
mina=min(mina,min1[i][i+n-1]);
maxa=max(maxa,max1[i][i+n-1]);
}
printf("%d\n%d",mina,maxa);
return 0;
}
区间划分
对于 2D/1D 动态规划,使用决策单调性可以将复杂度从 优化至 。这种题目一般是这种形式:
这种类型的 2D/1D 动态规划满足决策单调性的要求为 要同时满足区间包含单调性和四边形不等式。对于这一类动态规划,决策单调性指如下式子:(记 的最优转移点为 )
可以使用与区间划分相同的方法优化,需要特别注意一下转移顺序,可以证明优化之后复杂度为 。
除此之外,我们发现对于每一层状态之间的转移没有依赖性,因此我们可以对每一层使用分治解决,时间复杂度为 。
我们定义函数 solve(l,r,lc,rc)
表示 到 的最优决策点的取值范围是 。我们在 中枚举,求出前一半 到 的最大最优转移点,即 的转移点 。之后,由于决策单调性,我们可以继续递归左半边 solve(l,mid-1,lc,d)
和右半边 solve(mid+1,r,d,rc)
。如果 ,表示空区间,直接返回。
一共进行了 层操作,每一层都是 的,总时间复杂度 。
代码中 表示 的值, 表示 的值。
void solve(long long l,long long r,long long lc,long long rc)
{
if(l>r)return;
long long mid=(l+r)>>1,d=0;
for(int i=lc;i<=min(mid,rc);i++)
if(g[i-1]+cal(i,mid)<f[mid])f[mid]=g[i-1]+cal(i,mid),d=i;
solve(l,mid-1,lc,d),solve(mid+1,r,d,rc);
}
注意上述程序调用的 cal(i,mid)
函数可能是一个需要 计算的东西,这个时候我们就需要用双指针挪动来计算这个式子。
具体的,我们如果上一次处理了 cal(l,r)
的值,我们就把这个结果保留下来。下一次计算 cal(lc,rc)
时,就把 挪动到 的位置,少的数贡献加进去,多的数贡献减出来。 做同样的操作挪动到 的位置。
第一次调用之后,枚举决策点时可以直接右移左端点,非常方便。
手玩一下二分的过程与这个双指针挪动的过程,发现每次挪动过程和当前处理的区间长度同阶,于是挪动的复杂度也是 的。
long long cal(long long lc,long long rc)
{
while(l>lc)l--,add(a[l],1);
while(r<rc)r++,add(a[r],1);
while(l<lc)add(a[l],-1),l++;
while(r>rc)add(a[r],-1),r--;
return sum;
}
例题 :
CF868F Yet Another Minimization Problem
设 表示位置 后分出第 个子段的费用之和的最小值,按照题意,不难推出如下 DP 方程式:
其中 表示区间 中相同元素的对数。不难发现这个函数满足四边形不等式与区间包含单调性,感性理解一下,因为较长段相同元素的对数是平方增长的,较长段大劣于较短段,包含肯定劣于交叉,满足四边形不等式。或者考虑区间右端点右移的增量。而区间包含单调性是显然的,所以满足区间划分类型的决策单调性。
似乎不可以直接使用例题 中比较简单的转移方式,因为 似乎并不是很好求。因此,我们考虑分治做法。
之后就是把上面讲的分治做法写一遍。处理 cal(i,mid)
时使用类似莫队的思想维护区间中相同元素的对数。就是改变某个数出现次数之前先撤销影响,然后改变这个数出现次数,最后把这个数的影响加回去。
#include <bits/stdc++.h>
using namespace std;
long long n,k,a[200000],f[200000],g[200000],t[200000],sum=0,l=1,r=0;
bool b[200000];
void add(long long x,long long k)
{
sum-=t[x]*(t[x]-1)>>1;
t[x]+=k;
sum+=t[x]*(t[x]-1)>>1;
}
long long cal(long long lc,long long rc)
{
while(l>lc)l--,add(a[l],1);
while(r<rc)r++,add(a[r],1);
while(l<lc)add(a[l],-1),l++;
while(r>rc)add(a[r],-1),r--;
return sum;
}
void dfs(long long l,long long r,long long lc,long long rc)
{
if(l>r)return;
long long mid=(l+r)>>1,d=0;
for(int i=lc;i<=min(mid,rc);i++)
{
if(g[i-1]+cal(i,mid)<f[mid])f[mid]=g[i-1]+sum,d=i;
if(b[i])add(a[i],-1),b[i]=0;
}
dfs(l,mid-1,lc,d),dfs(mid+1,r,d,rc);
}
int main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)g[i]=f[i]=1e18;
g[0]=0;
for(int i=1;i<=k;i++)
{
dfs(1,n,1,n);
for(int j=1;j<=n;j++)g[j]=f[j],f[j]=1e18;
}
printf("%lld\n",g[n]);
return 0;
}
后记
谁家捣药的月兔顾影自怜
幻化成人红尘间流连
说书的将神话坊间传遍
殊不知逗笑了 哪路神仙
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探