决策单调性优化DP总结
1D1D
转移代价函数满足交叉优于包含
,即可推出具有决策单调性。
然后比较一般的做法是,根据每个点能够作为被决策点的是一段区间,且区间随着点连续右移,可以用单调队列维护决策点,每次加入时弹出队尾所有能够完全覆盖的,然后到第一个不完全覆盖的,二分找出区间左端点。
对于被决策点不会成为决策点的情况,也可以用分治解决,但单调队列+二分的做法最为普适。
值得注意的是,如果查询的代价函数需要的计算,那分治对比单调队列二分(需要比较两个决策点,且查询比起分治更不连续)能减小一半以上的常数,建议用分治(详见第三题)。
2D1D(区间类)
要求转移代价函数满足区间包含单调性
+交叉优于包含
。
详见第二题。
两道做过的思路类似的题
ICPC2018NJ B
因为恰好选k个
这个限制,肯定没法在DP内直接做(如果不增加一维状态);所以考虑wqs二分:对于选取的个数k来说,答案是随k增大而减小的;而如果给选取的每个点加上一个代价x,那么k会随着x增大减小,即答案随着x递增,我们要求的是满足k为给定的数且答案最小,那么二分x即可。
二分完之后,列出朴素DP式,发现代价满足四边形不等式,用普通的单调队列+二分优化DP即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=3e5+5;
const ll F=1e15;
int n,k;
ll a[N],s[N];
ll f[N]; int g[N];
struct node{
int i,l;
}q[N];
ll cnt(int l,int r){
int mid=(l+r)>>1;
return a[mid]*(mid-l+1)-(s[mid]-s[l-1])+(s[r]-s[mid])-a[mid]*(r-mid);
}
int dp(ll x){
//cout<<"x="<<x<<endl;
int ft=0,tl=0;
q[++tl]=(node){0,1};
for(int i=1;i<=n;i++){
if(ft+1<tl && q[ft+2].l<=i) ft++;
int d=q[ft+1].i;
f[i]=f[d]+cnt(d+1,i)+x;
g[i]=d;
// cout<<i<<" "<<f[i]<<" "<<g[i]<<endl;
while(ft<tl){
node u=q[tl];
if(u.l>i && f[i]+cnt(i+1,u.l)<=f[u.i]+cnt(u.i+1,u.l)){
tl--; continue;
}
int l=max(u.l,i+1),r=n;
while(l<r){
int mid=(l+r)>>1;
if(f[i]+cnt(i+1,mid)<=f[u.i]+cnt(u.i+1,mid)) r=mid;
else l=mid+1;
}
if(l<=n && f[i]+cnt(i+1,l)<=f[u.i]+cnt(u.i+1,l)) q[++tl]=(node){i,l};
break;
}
}
int c=0,u=n;
while(u) c++,u=g[u];
return c-1;
}
int main()
{
//srand(time(0));
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
//int T; cin>>T; while(T--) work();
cin>>n>>k;
for(int i=1;i<=n;i++) scanf("%d",&a[i]),s[i]=s[i-1]+a[i];
ll l=0,r=F;
while(l<r){
ll mid=(l+r+1)>>1;
if(dp(mid)>=k) l=mid;
else r=mid-1;
}
// cout<<"l="<<l<<endl;
dp(l);
cout<<f[n]-l*k<<endl;
return 0;
}
2022暑假牛客多校A
上一题的hard版,利用了DP状态的一些优美性质。
原本看到恰好选k个,直接的反应就是wqs二分,然后就不会了。
沿着wqs二分的思路,理论上也是能做的(确实有人卡过去了orz)
对于二维平面上n个点的凸包选取一个子集,代价为选取的点中每相邻两个点的距离
,求最大代价的问题:容易想到floyd求最长路以及对应的DP形式f[l][r]=max{f[l][k]+w[k][r]}
,然而是。
这时候考虑决策单调性,发现代价w满足四边形不等式,所以就具有决策单调性。
而通常的决策单调性优化DP是用单调队列+二分,可以把一个n优化为logn;在本题中,因为DP状态是区间的形式,可以直接利用这个性质,按照区间长度枚举,每个区间长度对应的范围之和为n,就把DP优化到了!
但直接用wqs二分容易被卡精(原本被卡到自闭),又注意到此题的DP状态可以写成矩阵的形式,而要求恰好k步就是k个转移矩阵相乘!那么直接矩阵快速幂即可,容易发现在矩阵相乘的时候也是满足决策单调性的,优化之后即可做到
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2005;
const double F=1e9;
int n,k;
double x[N],y[N],d[N][N];
struct mtx{
double a[N][N],mx;
}p,s,t,q;
void init(mtx& A){
for(int i=1;i<=n;i++){
A.a[i][i]=0;
for(int j=i+1;j<=n;j++) A.a[i][j]=-F;
}
A.mx=0;
}
double cnt(int l,int r,int k){
return t.a[l][k]+q.a[k][r];
}
int ps[N][N];
void mul(mtx A,mtx B,mtx& C){
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
t.a[i][j]=A.a[i][j],q.a[i][j]=B.a[i][j];
init(C);
for(int len=1;len<=n;len++) for(int l=1;l+len-1<=n;l++){
if(len==1){
//make names clear!!!
C.a[l][l]=0;
ps[l][l]=l;
continue;
}
int r=l+len-1;
for(int j=ps[l][r-1];j<=ps[l+1][r];j++){
double x=cnt(l,r,j);
//cout<<j<<" "<<x<<endl;
if(x>C.a[l][r]){
C.a[l][r]=x;
ps[l][r]=j;
}
}
//printf("l=%d r=%d f=%lf g=%d p=%d\n",l,r,f[l][r],g[l][r],p[l][r]);
}
for(int i=1;i<n;i++) for(int j=i+1;j<=n;j++) C.mx=max(C.mx,C.a[i][j]+d[i][j]);
}
int main()
{
//srand(time(0));
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
//int T; cin>>T; while(T--) work();
cin>>n>>k;
for(int i=1;i<=n;i++) scanf("%lf%lf",&x[i],&y[i]);
for(int i=1;i<n;i++) for(int j=i+1;j<=n;j++)
d[i][j]=p.a[i][j]=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
k--;
for(;k;k>>=1,mul(p,p,p)) if(k&1) mul(s,p,s);
printf("%.10lf\n",s.mx);
return 0;
}
2023CCPC网络赛L
非DP问题的决策单调性。
首先按照b升序,考虑每个前缀i,对于每个k的贡献就是
对于每个k,考虑快速找到使其最优的决策点i:可以发现随着k增加,i必然不降;利用这个单调性即可分治求解,查询用主席树,效率(单调队列易被卡常)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+5,F=1e9+5;
const ll inf=1e18;
int n;
namespace seg{
struct{
ll x,s;
int l,r;
}a[N<<5];
int his[N],cnt,l0,r0;
void init(int l,int r){
l0=l,r0=r;
cnt=0;
}
void upd(int& u,int x,ll k,int l=l0,int r=r0){
a[++cnt]=a[u]; u=cnt;
if(l==r){
a[u].x++;
a[u].s+=k;
return;
}
int m=(l+r)/2;
if(x<=m) upd(a[u].l,x,k,l,m);
else upd(a[u].r,x,k,m+1,r);
a[u].x=a[a[u].l].x+a[a[u].r].x;
a[u].s=a[a[u].l].s+a[a[u].r].s;
}
//ll qry(int u,int x,int y,int l=l0,int r=r0)
ll kth(int u,ll k,int l=l0,int r=r0){
if(l==r) return min(a[u].x,k)*l;
int m=(l+r)/2;
ll lv=a[a[u].l].x;
if(k<=lv) return kth(a[u].l,k,l,m);
else return a[a[u].l].s+kth(a[u].r,k-lv,m+1,r);
}
}using namespace seg;
struct node{
int a,b;
}p[N];
bool cmp(node u,node v){
return u.b<v.b;
}
ll ans[N];
void div(int l,int r,int x,int y){
if(l>r) return;
if(l==r){
ll mn=inf;
int pos=0;
for(int i=max(x,l);i<=y;i++){
ll u=kth(his[i],l)+p[i].b;
if(u<mn) mn=u,pos=i;
}
ans[l]=mn;
return;
}
int mid=(l+r)/2;
// cout<<"l="<<l<<" r="<<r<<" mid="<<mid<<endl;
ll mn=inf;
int pos=0;
for(int i=max(x,mid);i<=y;i++){
ll u=kth(his[i],mid)+p[i].b;
if(u<mn) mn=u,pos=i;
}
ans[mid]=mn;
// cout<<"mn="<<mn<<" pos="<<pos<<endl;
div(l,mid-1,x,pos); div(mid+1,r,pos,y);
}
void work(){
cin>>n;
for(int i=1;i<=n;i++) scanf("%d%d",&p[i].a,&p[i].b);
sort(p+1,p+n+1,cmp);
init(1,F);
for(int i=1;i<=n;i++){
his[i]=his[i-1];
upd(his[i],p[i].a,p[i].a);
}
div(1,n,1,n);
for(int i=1;i<=n;i++) printf("%lld\n",ans[i]);
}
int main()
{
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
work();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!