决策单调性DP
决策单调性DP是一个非常重要的DP类别。在决策点随枚举点增加单调不降时,可以有效地优化复杂度。
一般而言,决策点指的是对于一个 \(f[i]\),它的值需要从另一个值j中转移,而对于所有j,令 \(f[i]\) 最大的j值就是决策点。
而其单调性体现在对于一个点i,它的决策点一定会大于等于i-1的决策点。如果此单调性成立,那么一般就会用二分缩小决策点值域或者一些单调的数据结构(一般为单调栈,有时也有单调队列)来减小复杂度。决策点的单调性大多数时候只能感性理解或者打表。在考场上证明最基本的需要四边形不等式。一旦证明卡住乃至错误,很有可能就会丢失大部分分数
下面就是处理决策单调性的两种方法———二分与单调数据结构
二分
Yet Another Minimization Problem
- 最朴素的DP就是设 \(f_{i,j}\) 代表对于前i个数,将其分为j段的最小代价
转移方程也是呼之欲出, \(f_{i,j}=min_{k<i}(f_{k-1,j-1}+cacl(k,i))\),其中 \(cacl(k,i)\) 指k到i的贡献 - 但就算可以 \(O(1)\) 求\(cacl\),时间复杂度也是 \(O(n^{2})\) 的。
考虑单调性。由于对于 \(cacl(k,i)\) ,其值是由\(cacl(k,i-1)+\sum_{j=k}^{i}[a_{j}=a_{i}]\) 转移而来的,因此对于点 \(i\) ,对于它之前的两个点 \(u,v(u<v)\) ,如果 \(i\) 从 \(u\) 转移过来的值 \(f_{u,j-1}+cacl(u+1,i)\) 比从 \(v\) 转移过来的值 \(f_{v,j-1}+cacl(v+1,i)\) 大,那么对于点 \(i+1\) ,其 \(f_{i+1,j}\) 值如果从两者中转移,那对于 \(u\) 而言,\(f_{i+1,j}=f_{u,j-1}+cacl(u+1,i+1)\)。之前我们知道,\(cacl(a,b)\) 中随着 \(b\) 的增大,其值一定单调不降,所以从 \(v\) 转移有可能比从 \(u\) 转移更优,因为尽管 \(i\) 从 \(u\) 转移更优,但 对于\(i+1\) 加的 \(cacl\) 值比 \(v\) 大,因此皆有可能。但对于 \(u\) 之前一个点,如果\(i\) 从其转移比 \(u\) 劣,那 \(i+1\) 从其转移一定比 从 \(u\) 转移劣。因此,\(i+1\) 的决策点一定大于等于 \(i\) 的决策点。 - 知道有单调性后,考虑到对于每个 \(f_{k,j}\) ,其决策点是独立的,只与上一层枚举的分为 \(j-1\) 段的 \(f_{1->k,j-1}\) 值有关,与同层之间的 \(f\) 值无关,因此直接考虑枚举将区间分为多少段,再二分每个 \(mid\),求出它的决策点后就知道了它前后点的决策点的范围,进一步缩小枚举范围。
void erfen(ll l,ll r,ll lt,ll rt)
{
ll mid=(l+r)>>1,ml=max(1ll,lt),mr=min(mid,rt),pos;
//这里注意mr的取值。我们要求的是mid的f值和其决策点的位置
//但mid的决策点的位置起码不应大于自己
ll res=inf;
for(ll i=ml;i<=mr;i++)
{
ll tmp=f[i-1][dep-1]+cacl(i,mid);
if(tmp<res) res=tmp,pos=i;
}
f[mid][dep]=res;
if(l==r) return;
erfen(l,mid,lt,pos);
erfen(mid+1,r,pos,rt);
}
- 但如果要想做到 \(O(kn\log_{2}n)\) 的复杂度,就还需要 \(O(1)\) 求 \(cacl\) 的值。考虑到对于上述代码中的每次二分的过程,在循环中枚举的左端点是连续上升的,而对于其枚举的左右区间,从 \(l,r\) 递归到 \(l,mid\),其询问的 \(cacl\) 的区间最多移动 \(r-l\) 次。所有区间垒起来会成为类似于线段树分割区间那样的形式。每递归一层 \(l,r\) 都会移动 \(n\) 次,而一共递归了 \(\log_{2}n\) 层,因此对于每一个枚举的分为的段数, \(l,r\) 总共移动了 \(n\log_{2}n\) 次。因此考虑像莫队一样用桶和左右指针相对暴力地维护 \(cacl\) 的值。而二分的复杂度也是 \(O(n\log_{2}n)\) 的,因此总复杂度就为 \(O(kn\log_{2}n)\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf=1e18;
const ll N=1e6+10;
ll n,k,dep=0,a[N],f[N][25],sum=0,cnt[N],L=1,R=0;
ll cacl(ll lt,ll rt)
{
while(L>lt) sum+=cnt[a[--L]]++;
while(R<rt) sum+=cnt[a[++R]]++;
while(L<lt) sum-=--cnt[a[L++]];
while(R>rt) sum-=--cnt[a[R--]];
return sum;
}
void erfen(ll l,ll r,ll lt,ll rt)
{
ll mid=(l+r)>>1,ml=max(1ll,lt),mr=min(mid,rt),pos;
ll res=inf;
for(ll i=ml;i<=mr;i++)
{
ll tmp=f[i-1][dep-1]+cacl(i,mid);
if(tmp<res) res=tmp,pos=i;
}
f[mid][dep]=res;
if(l==r) return;
erfen(l,mid,lt,pos);
erfen(mid+1,r,pos,rt);
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++) f[i][0]=inf;
for(int i=1;i<=n;i++) cin>>a[i];
while(dep<=k)
{
dep++;
erfen(1,n,1,n);
}
cout<<f[n][k]<<endl;
return 0;
}
P5574 [CmdOI2019] 任务分配问题
与上一道题相当类似,感觉有点套路但一开始还是一头雾水。
- 仍然考虑最简单的DP(但感觉很容易从这一步就卡住,要多找点感觉)
设 \(f_{i,j}\) 表示前 \(i\) 个任务用 \(j\) 台cpu的最小顺序对数,显然有转移:
\]
这里的cost指的是k+1到i的顺序对数。这个转移方程与上一道题几乎一模一样。
你随便打一下表,发现对于每一个固定的 \(j\),DP的转移都有单调性。那如何快速统计答案呢?考虑到此题可以支持 \(nk\log^2n\) 的复杂度,因此直接通过树状数组动态维护顺序对数。
而操作同样与上一道题雷同。因为可以保证复杂度,因此同样类似于莫队去一个一个挪统计答案,每一层最多挪 \(n\) 次,一共 \(\log n\) 层,每挪一次 \(\log n\)。
总时间复杂度 \(nk\log^2n\)。不过要注意一些细节,比如每一轮统计完答案记得初始化。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
const int N=3e4+7;
const int M=30;
int n,k,a[N],f[N][M],tr[N],jue[N],dep=0,L=1,R=0,val=0;
#define lowbit(x) (x&(-x))
void add(int x,int w){while(x<=n)tr[x]+=w,x+=lowbit(x);}
int query(int x){int res=0;while(x)res+=tr[x],x-=lowbit(x);return res;}
void move(int l,int r)
{
while(R<r) {++R;val+=query(a[R]),add(a[R],1);}
while(R>r) {add(a[R],-1),val-=query(a[R]);R--;}
while(L>l) {--L;val+=query(n)-query(a[L]);add(a[L],1);}
while(L<l) {add(a[L],-1),val-=query(n)-query(a[L]);L++;}
// cout<<L<<' '<<R<<' '<<val<<'\n';
}
void erfen(int l,int r,int ql,int qr)
{
int mid=(l+r)>>1,jue=ql,res=p*p,tmp=res,qrr=qr;ql=max(1ll,ql),qr=min(mid,qr);
for(int i=qr;i>=ql;i--)
{move(i,mid);tmp=f[i-1][dep-1]+val;if(res>=tmp) res=tmp,jue=i;}
f[mid][dep]=res;if(l==r) return;
erfen(l,mid,ql,jue),erfen(mid+1,r,jue,qrr);
}
signed main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;for(int i=1;i<=n;i++) cin>>a[i];
memset(f,0x3f3f3f,sizeof(f));f[0][0]=0;
while(dep<=k)
{
dep++;L=1,R=0,memset(tr,0,sizeof(tr));val=0;
erfen(1,n,1,n);
}
cout<<f[n][k]<<'\n';return 0;
}
单调数据结构
P1912 [NOI2009] 诗人小G
- 先想出最朴素的DP状态 \(f_{i}\) 表示前 \(i\) 首诗所能凑出的最小代价。
- 设 \(s_{i}=i+\sum_{j=1}^{i}len_{j}\),即前缀长度。加一是因为都要加空格。
方程 \(f_{i}=min_{j<i}(f_{j}+|s_{i}-s_{j}-1|^{P})\),减一是因为还要减去行末空格 - 显然暴力 \(O(n^{2})\),考虑如何优化。由于方程里有 \(P\) 次方项,不好数学方法转化或者数据结构维护,因此向单调性这方面靠。
- 可惜我不会证四边形不等式不如打表。可以发现每个点的决策点所组成的序列可能长成如下的情况:
112333334456
这里的数字对应的是每个点从哪个点转移过来最优,我们可以发现两条相当显然的性质:
- 每个点的决策点都一定小于自己
- 每个点做出决策后决策点一定不动,其 \(f\) 值一定也是确定了的(其实就是无后效性)
- 那我们就想到假设某个点的最优决策点已经找到,那可不可以在枚举到它的时候将其后面的点的最优决策点是它的点也一起处理了呢?
- 但这样的想法有一个问题,由前面的点更新后面的点很有可能后面的点又再次被更新,比如枚举到3的时候:
122|3333333
但枚举到4的时候:
1223|344444
- 这样我们就需要维护一个支持修改的单调的序列。同时,上面的过程中,竖线前的数我们已经处理了,需要被去掉。那么这个数据结构已经近在眼前了——单调队列。
- 这个单调队列不可能真的存下一整个真的完整序列,也没必要。我们考虑去存每一段连续的相同的区间的左右端点。设处理到一个点 \(i\),其决策点为 \(q_{head}\),就将决策点右端点小于 \(i\) 的区间删掉,然后在其右边的区间里二分从哪个点开始转移更优。比如说对于上面枚举到4的这个例子,我们要找的就是其左边这个点的决策点不是4而其本身决策点是4的加粗的这个点
1223|344444
由于这个点右边的所有点从4转移都比3更优,因此就可以二分。 - 不过二分有几个需要注意的点:
- 可能有某些决策点的整个区间都被覆盖完
如:
从12233|33345
到122333|6666
这时,就需要先判断单调队列末尾的区间的左端点从原来决策点转移与从现在这个点转移的优劣。
如果从原来转移优,那就在这个区间里二分(之前说的断点一定在这个区间里)
否则,这个区间一定会被现在枚举的这个点完全覆盖,因此直接删除即可(因为除非极劣,否则新加入的这个区间右端点一定是n) - 可能这个决策点很劣,完全覆盖不了
这种情况就在做上一步之前特判一下最后一个点从它原来的决策点转移好还是从当前枚举的点转移好就行了。
- 输出并不是主要要讲的,但还是有些坑点,这里提一下。
1.要用long double
,因为中间并不是最优的决策点的答案超过 \(1e18\),因此需要用long double
舍弃一些精度来提高存储量(long double
会自动帮你用科学计数法舍弃一些精度来存储数据)
2.在二分的时候需要对每个区间存储一下每个点对应的决策点位置,称为 \(lst_{i}\),再用其更新 \(nxt_{i}\) 来表示在哪一个数的位置换行,依次输出,注意 \(nxt_{i}\) 是反着更新的 - 时间复杂度的话,对于枚举的每个点,其最多入队、出队一次,对于枚举的每个点最差情况下都要二分,因此会算 \(n\log_{2}n\) 次 \(|s_{i}-s_{j}-1|^{P}\),而这个东西还需要快速幂 \(\log_{2}n\) 去算,因此总复杂度为\(O(Tn\log_{2}^{2}n)\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ld;
typedef long long ll;
//#define int ll
const ld inf=1e18*1.0;
const ll N=2e5+10;
ll n,L,P,len[N],sum[N],tail,head,l[N],r[N],q[N];
ll lst[N],nxt[N];
ld f[N];
string s[N];
ld ksm(ld x)
{
ld res=1;ll k=P;
while(k)
{
if(k&1) res=res*x;
x*=x,k>>=1;
}
return res;
}
#define getf(x,y) ((ld)((ld)f[y]+ksm((ld)abs(sum[x]-sum[y]-L-1))))
void shuru()
{
cin>>n>>L>>P;
for(ll i=1;i<=n;i++)
{
cin>>s[i];
len[i]=(ll)s[i].length()+1;
sum[i]=sum[i-1]+len[i];
}
}
void erfen(ll x)
{
ll now=q[tail],lt=l[now],rr=n;
while(lt<rr)
{
ll mid=(lt+rr)>>1;
if(getf(mid,now)>getf(mid,x)) rr=mid;
else lt=mid+1;
}
r[now]=lt-1,q[++tail]=x;l[x]=lt,r[x]=n;//更新左右区间范围
}
void cacl()
{
head=tail=0;
q[0]=0,l[0]=1,r[0]=n;
for(ll i=1;i<=n;i++)
{
while(r[q[head]]<i) head++;
ll now=q[head];
f[i]=getf(i,now);lst[i]=now;
if(getf(n,q[tail])<getf(n,i)) continue;
while(getf(l[q[tail]],q[tail])>getf(l[q[tail]],i)) tail--;
erfen(i);
}
}
int T;
void write()
{
if(f[n]>inf) cout<<"Too hard to arrange"<<'\n';
else
{
cout<<(ll)(f[n]+0.5)<<'\n';
for(ll i=n;i;i=lst[i]) nxt[lst[i]]=i;//注意i是倒着跳lst的
ll now=0;
for(ll i=1;i<=n;i++)
{
now=nxt[now];
for(ll j=i;j<now;j++) cout<<s[j]<<' ';
cout<<s[now]<<'\n';
i=now; //i不用再加1了,for会帮你加
}
}
// puts("--------------------");
if(!T)
cout<<"--------------------";
else cout<<"--------------------"<<'\n';
}
signed main()
{
ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
cin>>T;
while(T--)
{
shuru();
cacl();
write();
}
}
决策单调性DP的更多相关文章
- BZOJ4426 :最大生产率(贪心+决策单调性DP)
题意:给出N个人,现在让你分P组,每组的工作效率是最小结束时间-最大开始时间,要求每一组的效率的正数,求最大效率和.N<1000 思路: 把包含至少一个其他的分到A组:否则到B组. A组的要么单 ...
- CF321E Ciel and Gondolas 【决策单调性dp】
题目链接 CF321E 题解 题意:将\(n\)个人分成\(K\)段,每段的人两两之间产生代价,求最小代价和 容易设\(f[k][i]\)表示前\(i\)个人分成\(k\)段的最小代价和 设\(val ...
- BZOJ2216 [Poi2011]Lightning Conductor 【决策单调性dp】
题目链接 BZOJ2216 题解 学过高中数学都应知道,我们要求\(p\)的极值,参变分离为 \[h_j + sqrt{|i - j|} - h_i \le p\] 实际上就是求\(h_j + sqr ...
- 洛谷 P3515 [ POI 2011 ] Lightning Conductor —— 决策单调性DP
题目:https://www.luogu.org/problemnew/show/P3515 决策单调性... 参考TJ:https://www.cnblogs.com/CQzhangyu/p/725 ...
- LOJ2074/2157 JSOI2016/POI2011 Lightning Conductor 决策单调性DP
传送门 我们相当于要求出\(f_i = \max\limits_{j=1}^{n} (a_j + \sqrt{|i-j|})\).这个绝对值太烦人了,考虑对于\(i>j\)和\(i<j\) ...
- Wannafly Camp 2020 Day 3F 社团管理 - 决策单调性dp,整体二分
有 \(n\) 个数构成的序列 \({a_i}\),要将它划分为 \(k\) 段,定义每一段的权值为这段中 \((i,j) \ s.t. \ i<j,\ a_i=a_j\) 的个数,求一种划分方 ...
- BZOJ1563:[NOI2009]诗人小G(决策单调性DP)
Description Input Output 对于每组数据,若最小的不协调度不超过1018,则第一行一个数表示不协调度若最小的不协调度超过1018,则输出"Too hard to arr ...
- 【bzoj4709】[Jsoi2011]柠檬 决策单调性+dp
Description Flute 很喜欢柠檬.它准备了一串用树枝串起来的贝壳,打算用一种魔法把贝壳变成柠檬.贝壳一共有 N (1 ≤ N ≤ 100,000) 只,按顺序串在树枝上.为了方便,我们从 ...
- bzoj 2216: [Poi2011]Lightning Conductor【决策单调性dp+分治】
参考:https://blog.csdn.net/clove_unique/article/details/57405845 死活不过样例看了题解才发现要用double.... \[ a_j \leq ...
- 【BZOJ1563】诗人小G(决策单调性DP)
题意:给定N,L,P,求f[N] sum[i]递增,L<=3e6,P<=10 思路:四边形不等式的证明见https://www.byvoid.com/zhs/blog/noi-2009-p ...
随机推荐
- Deepseek学习随笔(5)--- DeepSeek 在职场中的应用
自动化办公 在职场中,DeepSeek 可以帮助自动化办公流程,如生成日报.撰写邮件等: 日报生成:请根据今日工作内容生成一份日报 DeepSeek 会生成一份简洁的工作日报,帮助你总结当天的工作内容 ...
- 在Android源码中为APK编译系统权限
系统权限获取 打包为APK进行系统签名 对于 部分功能的访问需要使用到系统权限,需要 添加 android:sharedUserId="android.uid.system" 权限 ...
- Linux Vim 最全面教程:从入门到精通
一.引言 Vim 是一款功能强大且在 Linux 系统中广泛使用的文本编辑器.它有着高效的编辑模式.丰富的快捷键以及众多强大的功能,对于想要深入学习 Linux 系统操作以及进行文本处理相关工作的新手 ...
- JS代码执行
- 变量命名不规范&我被deepseek骗了
首先是一个实体类 @Data public class Dto {private String mNumber; } 前端传来{"mNumber:"123"}为null的 ...
- [tldr] GO泛型编程
最少的内容简述如何在GO中使用泛型编程 函数泛型 func f[T any](s Set[T]) { } 在函数声明的时候添加一个[]作为泛型的说明, 在使用的时候是可以自动推断 很多时候, any的 ...
- 面试题-Java虚拟机
前言 Java虚拟机部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的帮 ...
- Arrays工具类--java进阶day06
1.Arrays工具类 这些方法都是针对数组,并且都被static修饰,可以直接使用类名进行调用 1.toString 将数组拼接成带有相对应格式的字符串,可用于展示数组 2.equals 比较两个数 ...
- 【JDBC第3章】使用PreparedStatement实现CRUD操作
第3章:使用PreparedStatement实现CRUD操作 3.1 操作和访问数据库 数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果.其实一个数据库连接就是一 ...
- Tampermonkey 油猴脚本中文手册(出处:https://www.itblogcn.com/article/2233.html)
文章目录 @name @namespace @copyright @version @description @icon, @iconURL, @defaulticon @icon64, @icon6 ...