ABC133F Small Products

考虑 DP。

状态

f[][x] 表示长度为 ,首项不超过 x 的序列的个数。
答案是 f[K][N]
有递推 f[][x]=f[][x1]+f[1][\floorN/x]。照这个递推式求解,复杂度度太高;把它改成
f[][x]=y=1xf[1][\floorN/y] 也就是枚举首项。
我们的目标是求出 f[K][N],结合递推式来看,可以发现我们需要计算的状态的第二维都可以写成 \floorN/i。而我们熟知 \floorN/i 的不同取值不超过 2N 个。因此需要计算的状态不超过 2KN 个。

先来解决状态表示的问题,也就是 \floorN/i 的表示问题。虽然 \floorN/i 的取值不超过 2N 个,但是不能直接以 \floorN/i 作为数组下标。可以这样做,对于 iN,用 i 表示 \floorN/i,对于 iN,直接以 \floorN/i 作为下标。从代码实现的角度说就是开两个数组,f1[1..K][1..\floorN],f2[1..K][1..\floorN]f1[][i]:=f[][i]f2[][i]:=f[][\floorN/i]

注①:当 N 是完全平方数时,iNiN 这两段中都含有 N,这并不会造成问题。实际上分段时两侧都取等号是有意为之,这样可以使得递推式更简洁并且没有 corner case。这种分段方法适用于许多跟 \floorN/i 相关的分块问题。

注②:关于上一段所说的“对于 iN,用 i 表示 \floorN/i”,我们不需要关心 i\floorN/i 是不是单射。这里所谓“表示 \floorN/i”是说设计一种方法来把所有需要计算的 f[][\floorN/i] 紧凑地存到数组里并且可以快速地由 ,i 这两个 key 查到 f[][\floorN/i] 的值。不过可以证明,对于 iNi\floorN/i 确实是单射。

递推

对于 f1,有递推
f1[l][x]=f1[l][x1]+f[l1][\floorN/x]
由于 1x\floorN,有 f[l1][\floorN/x]=f2[l1][x],从而有
f1[l][x]=f1[l][x1]+f2[l1][x]

对于 f2,有递推式
f2[l][i]=f2[l][i+1]+x=\floorN/(i+1)+1\floorN/if[l1][\floorN/x]
容易证明下列几个不等式

  1. \floorN/i\floorN/(i+1)
  2. \floorN/\floorN/ii
  3. \floorN/(\floorN/i+1)<i

只证第 3 个。
设 $ \floor{\frac{N}{i}} = t t \le \frac{N}{i} < t + 1 \iff it \le N < i(t + 1) \iff i\frac{t}{t + 1} \le \frac{N}{t + 1} < i \implies \floor{\frac{N}{t + 1}} < i$

因此我们有 i\floorNx<i+1,即对于 \floorNi+1<x\floorNi 恒有 \floorNx=i,这里我们得到一个很有用的等式

\floorNi\floorNi+1,则
f[l][\floorNi]=f[l][\floorN/(i+1)]+(\floorNi\floorNi+1)f[l1][i]
并且当 \floorNi>\floorNi+1 时,i 可表为 \floorN\floorNi

从而有
f2[l][i]=f2[l][i+1]+(\floorNi\floorNi+1)f[l1][i]=f2[l][i+1]+(\floorNi\floorNi+1)f1[l1][i]

f2 的边界条件有两个:
1.
f2[1][i]=\floorNi
2.
f2[l][\floorN]:=f[l][\floorN\floorN]=f[l][\floorN\floorN+1]+(\floorN\floorN\floorN\floorN+1)f[l1][\floorN]=f1[l][\floorN\floorN+1]+(\floorN\floorN\floorN\floorN+1)f1[l1][\floorN]

代码

int main() {

    int n, k;
    scan(n, k);
    int r = sqrt(n + 0.5); // r is defined to be floor(sqrt{n})
    vv<int> f1(k + 1, vi(r + 1)); // f1[len][i]:长为len,首项 <= i
    vv<int> f2(k + 1, vi(r + 1)); // f2[len][i]:长为len,首项 <= n/i
 
    up (i, 1, r) {
        f1[1][i]=i;
    }
    up (i, 1, r) {
        f2[1][i] = n / i;
    }
    up (l, 2, k) {
        up (i, 1, r) {
            f1[l][i] = f1[l][i - 1] + f2[l - 1][i];
            if (f1[l][i] >= mod) {
                f1[l][i] -= mod;
            }
        }
        f2[l][r] = f1[l][n/(r + 1)] + (ll)(n / r - (n / (r + 1))) * f1[l - 1][r] % mod;
        if (f2[l][r] >= mod) {
            f2[l][r] -= mod;
        }
        down (i, r - 1, 1) {
            f2[l][i] = f2[l][i + 1] + (ll)(n / i - (n / (i + 1))) * f1[l - 1][i] % mod;
            if (f2[l][i] >= mod) {
                f2[l][i] -= mod;
            }
        }
    }
    println(f2[k][1]);
 
    return 0;
}

从另一个角度看待这个问题。以下所有 / 运算都向下取整。
取一个数字 m,求出 f[L][1..m]
f[L][i] = f[L][i-1] + f[L-1][N/i]
开一个数组 g[1..m],g[L][i] := f[L][N/i]
问题归结为如何计算 g[L][i]
上面已经得到
g[L][i] = g[L][i+1] + (L/i - L/(i+1))*f[L-1][i]
整个计算过程如下
for i = 1 to m
f[1][i] = i
g[1][i] = N/i
for L = 1 to K
f[L][0] = 0
for L = 2 to K
for i = 1 to m
f[L][i] = f[L][i-1] + g[L-1][i]
// compute g[L][m]
for i = m - 1 down to 1
g[L][i] = g[L][i+1] + (L/i - L/(i+1)) * f[L-1][i]
问题进一步归结为如何计算 g[L][m],即 f[L][N/m]
若 N/m <= m 则 f[L][N/m] 已经算出来了,不成问题。
若 N/m > m 但 N/(m + 1) <= m 则 f[L][N/m] = f[L][N/(m+1)] + (N/m - (N/(m+1))*f[L-1][m],也不成问题。
所以保险的办法是取 m 使得 N/(m + 1) <= m,取 m = floor(sqrt(N)) 就可以保证 N/(m+1) <= m。证明:m+1 > sqrt(N) 因此 N/(m+1) < sqrt(n) <= m 。
取 m = floor(sqrt(N)) + 1 可以保证 N / m < m。证明 m > sqrt(N),所以 N / m < sqrt(N) <= floor(sqrt(N)) < m。

posted @   Pat  阅读(261)  评论(0编辑  收藏  举报
编辑推荐:
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
阅读排行:
· C# 13 中的新增功能实操
· Ollama本地部署大模型总结
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(4)
· langchain0.3教程:从0到1打造一个智能聊天机器人
· 用一种新的分类方法梳理设计模式的脉络
点击右上角即可分享
微信分享提示