题解 P1912 【[NOI2009]诗人小G】

· · 题解

upd on 2023.9.2 经评论区 @bingxin 提醒代码过不了样例,发现是添加注释时多删了如下一行导致的:

if(calc(i,n)>calc(q[r],n)) continue;

这会导致状态 i 在完全不优于目前队尾的情况下(可以发现 i 无法成为任何一个位置的最优决策),进行错误的决策替换。

另外一种修改方法如评论区 @20200131wyz 所提(此处为未认真对待每一条评论深感愧疚),将二分边界改为 n+1,这也起到了判断 i 究竟是否优于队尾的作用。修改后的版本采用这种方法。

另外再修改了一下排版以及部分概念。

什么是决策单调性优化 dp?

形如 dp_i=\min/\max(dp_j+f(j,i)) 这类的式子,令 t_i 为使得 dp_i 取到 \min/\maxj,(其实 t_i 就叫最优决策)若 t_i 具有单调性,则这个 dp 式子就满足决策单调性。

具体如何判断?

f_j(i) 为用 j 转移时 dp_i 的值(即 dp_j+f(j,i))。

若对于任意 j_1<j_2f_{j_1}(i)f_{j_2}(i) 的函数图像至多有一个交点,在这个交点之前是 f_{j_1}(i) 更小,在这个交点后是 f_{j_2}(i) 更小,那么这个问题就满足决策单调性。

因为交点之前的 i 的决策都是 j_1,交点后的决策都是 j_2。正好满足决策单调性。

(其实如果反之,在交点前都是 j_2 更优而交点后都是 j_1 更优,是一种不同的决策单调性问题,因与本题无关此处略去。)

决策单调性的优化方法大致有决策栈,决策队列和分治。这题就是用决策队列优化。

决策队列具体是这样做的:

队列维护决策三元组 (p,l,r) 表示目前看来 t_l,t_{l+1}\dots t_r 都是 p。一般来说一开始队列里只有三元组 (0,1,n)

每次计算 dp_i 时,就先把队列中无用的三元组弹出(r<i),并把新的队首的 l 设为 i。此时 i 在队首,所以 i 的决策就是队首的 p。用 dp_p 更新 dp_i

接下来从队尾开始扫,如果用 i 来更新队尾的 l 比队尾的 p 更新队尾的 l 更优,说明队尾的决策应该比 p 更大(因为我们从小到大枚举 i),那么根据决策单调性,整个三元组中的决策都比 p 大。于是便可以弹出队尾。

到最后用 i 来更新队尾的 l 没有队尾的 p 更新队尾的 l 优。那么我们就要找出决策变为 i 的分界点。由于决策具有单调性,我们可以二分。二分边界就是队尾三元组的 ln(为什么不是 r?因为可能这整个三元组都满足原来的决策,此时二分却会把最后一个元素拆走)。

二分出分界点 x 后,队尾三元组的 r 应改为 x-1(之后的决策都要变)。队尾再新加入一个三元组 (i,x,n),表示 xn 的决策都可以改为更优的 i

至此算法结束。

因为每一个 i 都会至多添加一个三元组,对一个三元组进行二分,又因为一个三元组至多被删除一次,所以复杂度为 O(n\log n)

回到原题。

$s_i$ 表示前 $i$ 句话的总长度 $+i$(因为相邻句子要加空格) $$dp_i=\min_{1\le j<i}(dp_j+|s_i-s_j-l-1|^p)$$ 来解释一下这个式子。将第 $j+1$ 句到第 $i$ 句放一行,长度为 $s_i-s_j-1$ (行末不需要空格)根据题意,不协调度要新加 $|s_i-s_j-1-l|^p$。 现在看这一堆东西。发现正好满足决策单调性。(自己动手画个图吧,应该不难理解) 那么就可以上决策队列了。时间复杂度 $O(Tn\log n)$。具体见代码。 坑点: - 虽然题目说了答案大于 $10^{18}$ 就输出 `Too hard too arrange`,但是 dp 途中就超了怎么办?只能用上 `long double` 了。 - `cmath` 里的 `pow` 太慢,自己手写快速幂。 上代码:(~~详细~~注释) ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; typedef long double ld; const int maxn=111111; #define FOR(i,a,b) for(int i=(a);i<=(b);i++) #define ROF(i,a,b) for(int i=(a);i>=(b);i--) #define MEM(x,v) memset(x,v,sizeof(x)) inline int read(){ char ch=getchar();int x=0,f=0; while(ch<'0' || ch>'9') f|=ch=='-',ch=getchar(); while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar(); return f?-x:x; } int t,n,l,p,trans[maxn],ans[maxn],al,q[maxn],h,r,lft[maxn],rig[maxn]; ld pre[maxn],dp[maxn]; char s[maxn][33]; inline ld qpow(ld a,int b){ //手写快速幂 ld ans=1; for(;b;b>>=1,a*=a) if(b&1) ans*=a; return ans; } inline ld calc(int x,int y){ //从x转移到y return dp[x]+qpow(abs(pre[y]-pre[x]-l-1),p); } int main(){ t=read(); while(t--){ MEM(dp,0);MEM(s,0);MEM(trans,0);MEM(lft,0);MEM(rig,0);MEM(pre,0); n=read();l=read();p=read(); FOR(i,1,n) scanf("%s",s[i]+1),pre[i]=pre[i-1]+strlen(s[i]+1)+1; //pre就是长度的前缀和 q[h=r=1]=0;lft[0]=1;rig[0]=n; //实际代码实现时,不需要真的把一个三元struct放进队列,只要把决策编号放进去 //lft[i]和rig[i]表示以i为决策的区间,表示成上文中的三元组就是(i,lft[i],rig[i]) FOR(i,1,n){ while(h<=r && rig[q[h]]<i) h++; //弹出无用三元组 dp[i]=calc(q[h],i); //用队首决策转移 trans[i]=q[h]; while(h<=r && calc(i,lft[q[r]])<=calc(q[r],lft[q[r]])) r--; //i比目前队尾决策优,队尾需要替换 int lll=lft[q[r]],rrr=n+1; //二分 while(lll<rrr){ int mid=(lll+rrr)>>1; if(calc(i,mid)<=calc(q[r],mid)) rrr=mid; //i更优,说不定可以往前继续找 else lll=mid+1; //i不优,分界点一定在后面 } if(lll>n) continue; //i完全没有用,不管 rig[q[r]]=lll-1; //原来的决策只能管到rig-1,后面的归i管 q[++r]=i; lft[i]=lll;rig[i]=n; //新的三元组 } if(dp[n]>1e18) puts("Too hard to arrange"); else{ printf("%lld\n",ll(dp[n])); al=0; for(int i=n;i;i=trans[i]) ans[++al]=i; //沿着转移路线输出 int cur=al,tmp=0; FOR(i,1,n){ if(tmp) putchar(' '); //注意要不要加空格 printf("%s",s[i]+1); if(i==ans[cur]) putchar('\n'),cur--,tmp=0; else tmp++; } } puts("--------------------"); } } ```