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