CF1603C 题解

题意简述

给定一个数列,每次可以把一个数分拆成两个正整数,求它的每个子数组变成不降数列的最小操作数之和

思路

首先观察题目性质:对于最后一个数,我们不可能选择分拆它,这样只会让前面数的上限更小,进一步地,我们不可能分拆 \(a_i\leq a_{i+1}\) 的一段后缀

所以首先肯定是对 \(a_i>a_{i+1}\) 的最靠后的值进行分拆,对于每一个这样的值,从后向前决策

考虑如何分拆最优:显然对于一个数 \(a_i\) ,我们会把它分拆成多个正整数 \(b_1\leq b_2 \leq \dots \leq b_k \leq a_{i+1}\) ,为了满足 \(b_k\leq a_{i+1}\) ,要求 \(k \geq \lceil \frac{a_i}{a_{i+1}}\rceil\)

然后为了最大化 \(b_1\) ,我们会拆得尽量平均,满足 \(b_1 \geq \Big\lfloor\dfrac{a_i}{\lceil \frac{a_i}{a_{i+1}}\rceil}\Big\rfloor\)

不难发现,这样一直取下去,是贪心的最优方案

现在题目还让我们对于每一个子数组求答案,可以考虑对于每一个结束位置从后往前暴力模拟,是 \(\mathcal{O}(n^2)\)

再次观察到本题值域不大,以及对于已经决策过的后缀对前面的数的影响只是一个上限 \(a_{i+1}\) ,所以考虑 DP ,对于每一个 \(i\)\(a_{i+1}\) 的值设置状态 \(f_{i,j}\) ,表示后缀分拆的方案数(为什么是方案数?因为直接对于题目中的操作数进行转移并不好统计最小的答案之和),然后再在转移时对于答案贡献 \(k\times f_{i,j}\times i\)\(k\) 是上文所述 \(k\)) ,其中乘上 \(i\) 是因为我们 DP 的是后缀,但是要对 \(i\) 个包含该后缀的子数组都有贡献

状态看起来是 \(\mathcal{O}(n^2)\) 级别的,但考虑所有的 \(k\) 都是像数论分块一样的式子,只有根号范围的取值,因此复杂度有保证

Code

#include <cstdio>
/*快读省略*/
const int _=100003;
const int P=998244353;
int n,res;
int f[_],g[_];
int a[_];
void upd(int &x,int y){if ((x+=y)>=P) x-=P;}
void solve(){
    n=read(); res=0;
    for (int i=1; i<=n; i++) a[i]=read();
    for (int i=n-1; i; i--){
        upd(f[a[i+1]],1);
        for (int l=1,r; l<=a[i+1]; l=r+1){
            int c=a[i+1]/l;
            r=a[i+1]/c;
            g[c]=f[c]; f[c]=0;
        }
        for (int l=1,r; l<=a[i+1]; l=r+1){
            int c=a[i+1]/l;
            r=a[i+1]/c;
            if (!g[c]) continue;
            int k=(a[i]+c-1)/c;
            upd(f[a[i]/k],g[c]);
            upd(res,1ll*g[c]*i%P*(k-1)%P);
        }
    }
    for (int l=1,r; l<=a[1]; l=r+1){
        int c=a[1]/l;
        r=a[1]/c;
        f[c]=0;
    }
    printf("%d\n",res);
}
int main(){
    int T=read();
    while (T--) solve();
    return 0;
}

后记

貌似是次劣解,由于直接把数论分块跑出来了……

官解的实现似乎用了两个 \(vector\) 进行迭代,跑不满 \(\mathcal{O}(n \sqrt {10^5})\) 的上界,因此快了不少,代码应该更好写

所以我的实现将就着参考一下吧……

感觉此题主要难在贪心的转化上,想到了贪心看一眼值域,DP 应该不难得出

果然是我贪心太菜

不会数论分块的可以 OI wiki

posted @ 2021-11-07 11:10  yyyyxh  阅读(66)  评论(0编辑  收藏  举报