-
决策单调性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(knlog_{2}n)\) 的复杂度,就还需要 \(O(1)\) 求 \(cacl\) 的值。考虑到对于上述代码中的每次二分的过程,在循环中枚举的左端点是连续上升的,而对于其枚举的左右区间,从 \(l,r\) 递归到 \(l,mid\),其询问的 \(cacl\) 的区间最多移动 \(r-l\) 次。所有区间垒起来会成为类似于线段树分割区间那样的形式。每递归一层 \(l,r\) 都会移动 \(n\) 次,而一共递归了 \(log_{2}n\) 层,因此对于每一个枚举的分为的段数, \(l,r\) 总共移动了 \(nlong_{2}n\) 次。因此考虑像莫队一样用桶和左右指针相对暴力地维护 \(cacl\) 的值。而二分的复杂度也是 \(O(nlog_{2}n)\) 的,因此总复杂度就为 \(O(knlong_{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;
}
单调数据结构
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}\) 是反着更新的 - 时间复杂度的话,对于枚举的每个点,其最多入队、出队一次,对于枚举的每个点最差情况下都要二分,因此会算 \(nlong_{2}n\) 次 \(|s_{i}-s_{j}-1|^{P}\),而这个东西还需要快速幂 \(log_{2}n\) 去算,因此总复杂度为\(O(Tnlog_{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();
}
}
标签:ll,mid,决策,枚举,lt,DP,单调
From: https://www.cnblogs.com/allforgod/p/18446029