决策单调性学习笔记
决策单调性,通过决策点的单调性进行处理,从而优化 dp 的时间复杂度
对于这种题目,可以分为几个过程来完成题目:
-
敲暴力
-
观察是否有其它的优化方式
-
分析最优决策点位置/打表
-
得到如何使用决策单调性优化
区间合并问题基本转移如下:
这类问题通常是可以通过决策单调性优化的,四边形不等式和区间单调性是通用解法,但是我认为具体题目具体分析会更方便一点。
石子合并这道题目就是一个典型,就算分析不好分析,但是直接看也能看出来怎样优化
石子的合并过程左右要基本类似,所以决策点应该在两个区间之间
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[2005],pre[2005][2005],dp[2005][2005],w[2005];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],w[i]=a[i]+w[i-1];
for(int i=n+1;i<=n*2;i++)a[i]=a[i-n],w[i]=a[i]+w[i-1];
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=2*n;i++)dp[i][i]=0,pre[i][i]=i;
int ans=1e18;
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
for(int k=pre[l][r-1];k<=pre[l+1][r];k++){
if(dp[l][k]+dp[k+1][r]+w[r]-w[l-1]<dp[l][r]){
dp[l][r]=dp[l][k]+dp[k+1][r]+w[r]-w[l-1];
pre[l][r]=k;
}
}
if(len==n)ans=min(ans,dp[l][r]);
}
}
cout<<ans;
return 0;
}
单调队列式斜率优化应该也是决策单调性的一种,但是专门讲过了。
能用斜率优化的一般都用斜率优化,比较快
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[10005],p[10005],head,tail,x[10005],y[10005],dp[10005];
bool slope(int i,int j,int k){
return (y[i]-y[j])*(x[j]-x[k])<=(y[j]-y[k])*(x[i]-x[j]);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
sort(a+1,a+n+1);
memset(dp,0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=m;i++){
for(int j=0;j<=n;j++)x[j]=2*a[j+1],y[j]=a[j+1]*a[j+1]+dp[j];
head=1,tail=0;
p[++tail]=i-1;
for(int j=i;j<=n;j++){
while(head<tail&&y[p[head+1]]-y[p[head]]<=a[j]*(x[p[head+1]]-x[p[head]]))head++;
dp[j]=y[p[head]]-a[j]*x[p[head]]+a[j]*a[j];
while(head<tail&&slope(j,p[tail],p[tail-1]))tail--;
p[++tail]=j;
}
}
cout<<dp[n];
return 0;
}
需要一个精妙的状态,表示 \(i\) 为最后一个邮局时前面的所有花费
然后两点之间的费用就能用二分求出来了
为什么会有决策单调性?
因为我们从左往右处理,那么我们不可能会把前面的点的决策点之前的点拿过来,这比起前面点的决策点增加的费用还大,不可能作为新的决策点
顺便讲一讲如何用单调队列来处理答案
首先使用单调队列,前提是必须要最佳决策点不断往右靠,我们加入一个值时,要对比一下两个不同的点之间在哪个点时优劣发生改变,让队列从头到尾越前面越在早的时候优,如果已经不是优的区间了再出队
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,head,tail,p[100005],dp[100005],d[100005],c[100005],w[100005],pre[100005],sum1[100005],sum2[100005],k[100005];
int query(int x,int y){
if(!x)return sum2[y]-pre[y]*(d[n]-d[y]);
int l=x,r=y;
while(l<=r){
int mid=l+r>>1;
if(d[y]-d[mid]>=d[mid]-d[x])l=mid+1;
else r=mid-1;
}
int k=l-1;
int res=sum1[k]-sum1[x-1]-(pre[k]-pre[x-1])*d[x]+sum2[y]-sum2[k]-(pre[y]-pre[k])*(d[n]-d[y]);
return res;
}
int calc(int x,int y){
return dp[x]+query(x,y)+c[y];
}
int ef(int x,int y){
int l=max(x,y),r=n;
while(l<=r){
int mid=l+r>>1;
if(calc(x,mid)>=calc(y,mid))r=mid-1;
else l=mid+1;
}
return r;
}
void insert(int x){
if(!tail)p[++tail]=x;
else {
while(head<tail&&k[tail-1]>=ef(p[tail],x))tail--;
k[tail]=ef(p[tail],x);
p[++tail]=x;
}
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>d[i];
for(int i=1;i<=n;i++)cin>>c[i];
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=1;i<=n;i++)pre[i]=pre[i-1]+w[i];
for(int i=1;i<=n;i++)sum1[i]=sum1[i-1]+w[i]*d[i];
for(int i=1;i<=n;i++)sum2[i]=sum2[i-1]+w[i]*(d[n]-d[i]);
head=1,tail=0;
for(int i=1;i<=n;i++){
insert(i-1);
while(head<tail&&k[head]<i)head++;
dp[i]=calc(p[head],i);
}
int ans=1e18;
for(int i=1;i<=n;i++)ans=min(ans,dp[i]+(sum1[n]-sum1[i-1])-d[i]*(pre[n]-pre[i-1]));
cout<<ans;
return 0;
}
首先进行一点点简单的数学转化,然后把状态中的两维中的一维从小到大枚举,另外一维就通过分治来处理,分治还是很好理解且好写的,但是要求前面转移的那几维已经知道了才能用
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[1005],pre[1005],sum[1005],dp[1005][1005];
void solve(int p,int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
int res=1e18,w;
for(int i=L;i<=R;i++){
int tmp=dp[i-1][mid-1]+(pre[p]-pre[i-1])*(pre[p]-pre[i-1])-(sum[p]-sum[i-1]);
if(tmp<res)res=tmp,w=i;
}
dp[p][mid]=res;
solve(p,l,mid-1,L,w);
solve(p,mid+1,r,w,R);
}
signed main(){
while(cin>>n>>m){
if(!n&&!m)break;
m++;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)pre[i]=pre[i-1]+a[i];
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i]*a[i];
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)solve(i,1,min(i,m),1,i);
cout<<dp[n][m]/2<<endl;
}
return 0;
}
类似的题目,我仍然拿分治处理,当然这题也可以斜率优化
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[3005],pre[3005],dp[3005][3005];
void solve(int p,int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
int res=1e18,w;
for(int i=L;i<=R;i++){
int tmp=dp[mid-1][i-1]+(pre[p]-pre[i-1])*(pre[p]-pre[i-1]);
if(tmp<res)res=tmp,w=i;
}
dp[mid][p]=res;
solve(p,l,mid-1,L,w);
solve(p,mid+1,r,w,R);
}
signed main(){
cin>>n>>m;
int s=0;
for(int i=1;i<=n;i++)cin>>a[i],s+=a[i];
for(int i=1;i<=n;i++)pre[i]=pre[i-1]+a[i];
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)solve(i,1,min(i,m),1,i);
cout<<dp[m][n]*m-s*s;
return 0;
}
可以用分治,当然单调队列也行,虽然求的是较大值,但是由于根号是越小的里面只增大改变越大,所以决策点还是向右靠的
代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[500005],dp[500005];
double sqr[500005];
double calc(int j,int i){
return a[j]+sqr[i-j];
}
void solve(int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
double res=0;
int c;
for(int i=L;i<=min(R,mid);i++){
if(calc(i,mid)>res)res=calc(i,mid),c=i;
}
dp[mid]=max(dp[mid],(int)ceil(res));
solve(l,mid-1,L,c);
solve(mid+1,r,c,R);
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
sqr[i]=sqrt(i);
}
solve(1,n,1,n);
reverse(a+1,a+n+1);
reverse(dp+1,dp+n+1);
solve(1,n,1,n);
for(int i=n;i>=1;i--)printf("%d\n",dp[i]-a[i]);
return 0;
}
终于来了一道不一样的,这道题目需要用栈来实现,因为平方的底数增加相同的数,这个底数越大,增加的越猛。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[100005],t[100005],sum[100005],dp[100005],op[100005];
vector<int> e[100005];
int calc(int x,int t){
return dp[x-1]+a[x]*t*t;
}
int fe(int x,int y){
int l=1,r=n,res=n+1;
while(l<=r){
int mid=l+r>>1;
if(calc(x,mid-sum[x]+1)>=calc(y,mid-sum[y]+1))res=mid,r=mid-1;
else l=mid+1;
}
return res;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],sum[i]=++t[a[i]];
for(int i=1;i<=n;i++){
int top=e[a[i]].size()-1;
while(top>=1&&fe(e[a[i]][top-1],e[a[i]][top])<=fe(e[a[i]][top],i))top--,e[a[i]].pop_back();
e[a[i]].push_back(i);
top++;
while(top>=1&&fe(e[a[i]][top-1],e[a[i]][top])<=sum[i])e[a[i]].pop_back(),top--;
dp[i]=calc(e[a[i]][top],sum[i]-sum[e[a[i]][top]]+1);
}
cout<<dp[n];
return 0;
}
01分数规划后通过分治处理出一半最大另一半最小值即可
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,pret[50005],prep[50005],f[50005],g[50005];
struct P{
int t,p;
}a[50005];
bool cmp(P a,P b){
return a.t*b.p>b.t*a.p;
}
void solve1(int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
f[mid]=1e16;
int w;
for(int i=L;i<=min(mid,R);i++){
int tmp=a[i].t*prep[mid]-a[i].p*pret[mid];
if(tmp<f[mid])f[mid]=tmp,w=i;
}
solve1(l,mid-1,L,w);
solve1(mid+1,r,w,R);
}
void solve2(int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
g[mid]=-1e16;
int w;
for(int i=max(L,mid+1);i<=R;i++){
int tmp=a[i].t*prep[mid]-a[i].p*pret[mid];
if(tmp>g[mid])g[mid]=tmp,w=i;
}
solve2(l,mid-1,L,w);
solve2(mid+1,r,w,R);
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i].t>>a[i].p;
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)pret[i]=pret[i-1]+a[i].t;
for(int i=1;i<=n;i++)prep[i]=prep[i-1]+a[i].p;
solve1(1,n-1,1,n);
solve2(1,n-1,1,n);
int ans=0;
for(int i=1;i<n;i++)if(f[i]<g[i])ans++;
cout<<ans<<endl;
for(int i=n-1;i>=1;i--)if(f[i]<g[i])cout<<n-i<<endl;
return 0;
}
把环拆成两个链,与自己环上的转移即可
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,x[1000005],y[1000005],dp[500005];
void solve(int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
int dis=0,w=0;
for(int i=max(L,mid);i<=min(mid+n,R);i++){
int tmp=(x[i]-x[mid])*(x[i]-x[mid])+(y[i]-y[mid])*(y[i]-y[mid]);
if(tmp>dis)dis=tmp,w=i;
}
dp[mid]=w;
if(dp[mid]>n)dp[mid]-=n;
solve(l,mid-1,L,w);
solve(mid+1,r,w,R);
}
signed main(){
cin>>T;
while(T--){
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld%lld",&x[i],&y[i]),x[i+n]=x[i],y[i+n]=y[i];
solve(1,n,1,2*n);
for(int i=1;i<=n;i++)printf("%lld\n",dp[i]);
}
return 0;
}
配合树上dp和期望dp而已
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,m,p;
double sum[1505],f[1505],c[1505][1505],dp[1505][1505];
vector<int> e[1505];
void dfs(int p){
f[p]=1;
for(int i:e[p]){
dfs(i);
f[p]+=f[i]/e[p].size();
}
}
void solve(int p,int l,int r,int L,int R){
if(l>r||L>R)return;
int mid=l+r>>1,w=L;
for(int i=L;i<=min(R,mid-1);i++){
if(dp[p-1][w]+c[w][mid]>dp[p-1][i]+c[i][mid])w=i;
}
dp[p][mid]=dp[p-1][w]+c[w][mid];
solve(p,l,mid-1,L,w);
solve(p,mid+1,r,w,R);
}
signed main(){
cin>>T;
while(T--){
cin>>n>>m>>p;
p--;
for(int i=1,x,y;i<=m-n;i++){
cin>>x>>y;
e[x].push_back(y);
}
for(int i=1;i<n;i++)e[i].push_back(i+1);
for(int i=1;i<=n;i++){
sum[i]=0;
for(int j:e[i])if(j>n)dfs(j),sum[i]+=f[j];
}
for(int i=1;i<=n;i++){
c[i][i]=0;
for(int j=i+1;j<=n;j++)c[i][j]=c[i][j-1]*e[j-1].size()+e[j-1].size()+sum[j-1];
}
for(int i=2;i<=n;i++)dp[0][i]=1e18;
dp[0][1]=0;
for(int i=1;i<=p;i++)solve(i,1,n,1,n);
printf("%.4lf\n",dp[p][n]);
for(int i=1;i<=m;i++)e[i].clear();
}
return 0;
}
抓住两个最优的性质开搞
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,ans;
struct P{
int l,r;
}a[1000005];
bool cmp(P a,P b){
if(a.l==b.l)return a.r>b.r;
return a.l<b.l;
}
int calc(int i,int j){
return (a[j].r-a[i].l)*max(a[i].r-a[j].l,0ll);
}
void solve(int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
int ma=0,w=0;
for(int i=L;i<=min(R,mid-1);i++){
int tmp=calc(i,mid);
if(tmp>ma)ma=tmp,w=i;
}
ans=max(ans,ma);
if(w){
solve(l,mid-1,L,w);
solve(mid+1,r,w,R);
}
else {
solve(l,mid-1,L,R);
solve(mid+1,r,L,R);
}
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld%lld",&a[i].l,&a[i].r);
sort(a+1,a+n+1,cmp);
int cnt=1;
for(int i=2;i<=n;i++){
if(a[i].r>a[cnt].r)a[++cnt]=a[i];
else ans=max(ans,(a[i].r-a[i].l)*(a[cnt].r-a[cnt].l));
}
solve(1,cnt,1,cnt);
cout<<ans;
return 0;
}
斜率优化里开的坑现在终于填上了,没什么特殊的
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,l,P,pre[100005],p[100005],nxt[100005],k[100005];
string s[100005];
long double dp[100005];
long double ksm(long double a,int b){
long double s=1;
while(b){
if(b&1)s=s*a;
a=a*a;
b>>=1;
}
return s;
}
long double calc(int i,int j){
return dp[j]+ksm(abs(pre[i]-pre[j]-l),P);
}
int ef(int x,int y){
int l=x,r=n+1;
while(l<r){
int mid=l+r>>1;
if(calc(mid,x)>=calc(mid,y))r=mid;
else l=mid+1;
}
return r;
}
signed main(){
cin>>T;
while(T--){
cin>>n>>l>>P;
l++;
for(int i=1;i<=n;i++)cin>>s[i];
for(int i=1;i<=n;i++)pre[i]=pre[i-1]+s[i].size()+1;
int head=1,tail=0;
p[++tail]=0;
for(int i=1;i<=n;i++){
while(head<tail&&k[head]<=i)head++;
dp[i]=calc(i,p[head]);
nxt[i]=p[head];
while(head<tail&&k[tail-1]>=ef(p[tail],i))tail--;
k[tail]=ef(p[tail],i),p[++tail]=i;
}
if(dp[n]>1e18)puts("Too hard to arrange");
else {
printf("%.0Lf\n",dp[n]);
}
}
return 0;
}
转化为 dp 再考虑决策单调性,发现要关注从哪些区间转移,考虑线段树分治,再在里面分治即可
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,T,d[200005],cnt,f[200005],dp[200005];
struct P{
int x,y;
}a[200005];
bool cmp(P a,P b){
return a.x<b.x;
}
vector<int> e[200005];
bool check(int x,int y){
return a[x].x<a[y].x&&a[x].y<a[y].y;
}
struct ST{
vector<int> c[800005];
int ls[800005],rs[800005],id;
void init(){
id=0;
}
int New(){
id++;
c[id].clear();
ls[id]=rs[id]=0;
return id;
}
void change(int &p,int l,int r,int L,int R,int x){
if(!p)p=New();
if(l>=L&&r<=R)return c[p].push_back(x);
int mid=l+r>>1;
if(mid>=L)change(ls[p],l,mid,L,R,x);
if(mid<R)change(rs[p],mid+1,r,L,R,x);
}
void CDQ(int p,int w,int l,int r,int L,int R){
if(l>r)return;
int mid=l+r>>1;
int k,mi=1e18;
for(int i=L;i<=R;i++){
int tmp=dp[e[w][i]]+(a[c[p][mid]].x-a[e[w][i]].x)*(a[c[p][mid]].y-a[e[w][i]].y);
if(tmp<mi)mi=tmp,k=i;
}
dp[c[p][mid]]=min(mi,dp[c[p][mid]]);
CDQ(p,w,l,mid-1,k,R);
CDQ(p,w,mid+1,r,L,k);
}
void query(int p,int w,int l,int r){
if(!p)return;
CDQ(p,w,0,c[p].size()-1,l,r);
int mid=l+r>>1;
query(ls[p],w,l,mid),query(rs[p],w,mid+1,r);
}
}seg;
int findl(int x,int id){
int l=0,r=e[id].size()-1;
while(l<=r){
int mid=l+r>>1;
if(a[e[id][mid]].y<a[x].y)r=mid-1;
else l=mid+1;
}
return r+1;
}
int findr(int x,int id){
int l=0,r=e[id].size()-1;
while(l<=r){
int mid=l+r>>1;
if(a[e[id][mid]].x<a[x].x)l=mid+1;
else r=mid-1;
}
return l-1;
}
signed main(){
cin>>n>>T;
for(int i=1;i<=n;i++)cin>>a[i].x>>a[i].y;
sort(a+1,a+n+1,cmp);
a[++n]={T,T},a[0]={0,0};
for(int i=0,w;i<=n;i++){
if(!cnt||a[i].y>d[cnt])w=++cnt;
else w=lower_bound(d+1,d+cnt+1,a[i].y)-d;
d[w]=a[i].y;
e[w].push_back(i);
}
memset(dp,0x3f,sizeof(dp));
dp[0]=0;
for(int i=2;i<=cnt;i++){
int rt=0;
seg.init();
for(int j:e[i])seg.change(rt,0,e[i-1].size()-1,findl(j,i-1),findr(j,i-1),j);
seg.query(rt,i-1,0,e[i-1].size()-1);
}
cout<<dp[n];
return 0;
}
小结:
发现死活优化不出来考虑有没有单调性,自己枚举情况推一下决策点范围是否和前面的决策点相关,若是推不出来就打表找规律

浙公网安备 33010602011771号