写一些简单的 1D/1D 的决策单调性优化...
这种决策单调性优化的过程:列出方程 $\to$ 打表发现决策单调性 $\to$ 套单调队列/二分
我们以一些例题为例:
<h2>[ZROI普转提]Bead</h2>
注:该题有权限,看不到的就看下一题。
这一题我们推出朴素的状态转移方程:
设 $f[i][k]$ 表示前 $i$ 个数中分成 $k$ 段的最小价值,转移为 $ f[i][k] = min{f[j-1][k-1] + w[j][i]} $,其中 $w[i][j]$ 表示 $i$ 到 $j$ 的价值。
显然 $ w[i][j] $ 可以通过维护 $cnt$ 数组 $N^2$ 求。
我们打表发现 $w$ 是递增的,说明该方程具有决策单调性。
决策单调性有一种较为简单的类似于整体二分的做法就是目前计算 $f[l\cdots r]$ 并且已知它们的决策都在 $[L, R]$ 中,每次暴力计算 $ f[mid] $ 即可。
只需要全局开两个指针 $i,j$ 和一个数组在每次计算 $ f[mid] $ 的时候移动过去并且维护 $cnt$ 即可。
复杂度 $O(nklog_2n)$.
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define LL long long #define U unsigned #define FOR(i,a,b) for(int i = a;i <= b;++i) #define RFOR(i,a,b) for(int i = a;i >= b;--i) #define CLR(i,a) memset(i,a,sizeof(i)) #define BR printf("--------------------n") #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const int MAXN = 100000 + 5; const int MAXK = 20 + 5; int N,K; int a[MAXN]; int fMAXN,cnt[MAXN],t[MAXN]; int L,R,ans; // fi = std::min(fi,fj-1 + wj); inline int query(int l,int r){ while(L > l) ans += cnt[a[--L]]++; while(R < r) ans += cnt[a[++R]]++; while(L < l) ans -= --cnt[a[L++]]; while(R > r) ans -= --cnt[a[R--]]; return ans; } void work(int k,int l,int r,int pl,int pr){ if(l > r) return; if(l == r){ FOR(i,pl,pr) fl = std::min(fl,fi-1 + query(i,l)); return; } int mid = (l + r) >> 1; FOR(i,pl,pr) t[i] = fi-1 + query(i,mid); int next = 0; FOR(i,pl,pr) if(!next || t[i] < t[next]) next = i; fmid = t[next]; work(k,l,mid-1,pl,next); work(k,mid + 1,r,next,pr); } int main(){ scanf("%d%d",&N,&K); FOR(i,1,N) scanf("%d",a+i); L = 1;R = 0; memset(f,0x7f,sizeof(f)); f0 = 0; FOR(i,1,K) work(i,1,N,1,N); printf("%dn",fN); return 0; }
[NOI2009]诗人小G
这一题我们可以轻松的推出朴素的状态转移方程:
设 $f[i]$ 表示前 $ i $ 个句子的最小不协调度,那么 $ f[i] = min{f[j] + |sum[j]-sum[i-1]-L-1|^P} $
我们随意的打个表,发现后面的那一项是递增的。(其实我也不会证明 qaq)
也就是说:我们在转移的时候,不需要考虑较大的状态,从小的状态转移过来。
这个显然可以用单调队列维护。
具体的说,每次我们计算出来的 $ f[i] $,假设它可以用来更新 $ f[j] $ ,那么对于 $j$ 及其以后的状态,都不用考虑 $1 \cdots i-1$ 的状态来转移了。那么我们要做的就是每次我们计算出 $ f[i] $ ,我们找到一个 $f[j]$ ,满足由 $i$ 转移到 $j$ 比$1\cdots i-1$ 更优。由于决策的单调性,可以二分来查找 $j$。最后用单调队列来保存一下决策即可。
<h3>代码</h3>
输出真毒瘤
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define R register #define LL long long #define U unsigned #define LD long double #define FOR(i,a,b) for(R int i = a;i <= b;++i) #define RFOR(i,a,b) for(R int i = a;i >= b;--i) #define CLR(i,a) memset(i,a,sizeof(i)) #define BR printf("--------------------n") #define DEBUG(x) std::cerr << #x << '=' << x << std::endl // f[i] = std::min(f[j] + (sum[i]-sum[j]L-1)^P) const int MAXN = 100000 + 5; const int MAXL = 30 + 5; int N,L,P; char strMAXN; int sum[MAXN],k[MAXN],pr[MAXN]; LD f[MAXN]; int q[MAXN],head,tail; inline LD qpow(LD a,int n){ LD ret = 1; while(n){ if(n & 1) ret = ret * a; a = a * a; n >>= 1; } return ret; } inline LD query(int i,int j){ return f[j] + qpow(std::abs(sum[i]-sum[j]-L),P); } inline int bound(int x,int y){ int l = x,r = N + 1; while(l < r){ int mid = (l + r) >> 1; if(query(mid,x) >= query(mid,y)) r = mid; else l = mid + 1; } return l; } inline void Solve(){ scanf("%d%d%d",&N,&L,&P);L++; FOR(i,1,N){ scanf("%s",str[i]); sum[i] = sum[i-1] + strlen(str[i]) + 1; } head = tail = 1; q[head] = 0; FOR(i,1,N){ while(head < tail && k[head] <= i) head++; f[i] = query(i,q[head]); pr[i] = q[head]; while(head < tail && k[tail-1] >= bound(q[tail],i)) --tail; k[tail] = bound(q[tail],i);q[++tail] = i; } if(f[N] > 1e18) puts("Too hard to arrange"); else{ printf("%.0Lfn",f[N]); tail = 0; q[tail] = N; int i = N; while(i){ q[++tail] = i = pr[i]; } while(tail){ for(i=q[tail]+1;i < q[tail-1];i++) printf("%s ",str[i]); puts(str[i]); tail--; } } puts("--------------------"); } int main(){ int T; scanf("%d",&T); while(T--){ Solve(); } return 0; }