DP的优化和根号分治: 跑步

NOI-Online 2020 跑步

题意

求整数可重复划分方案数, n105.

50

状态 fi,j 的表示数字 i 的划分中, 最大的数是 j 的方案数.

5 5 2 1 就是一个包括在 f13,5 中的一个划分方案.

fi,j=k=1kjfij,k

每个状态 fi,j 由所有 fij,k, k[1,j] 的总和组成. 可以理解成在 fij,k 的每一个划分方案中加上一个元素 j 得到一个对应的 i 的划分.

O(n2) 的状态, O(n) 的转移, 复杂度 O(n3).

for (register unsigned i(1); i <= n; ++i) {
  f[i][i] = 1;
}
for (register unsigned i(2); i <= n; ++i) {
  for (register unsigned j(1); j < i; ++j) {
    for (register unsigned k(1); k <= j; ++k) {
      f[i][j] += f[i - j][k];
      if(f[i][j] >= p) f[i][j] -= p;
    }
  }
}
for (register unsigned i(1); i <= n; ++i) {
  Ans += f[n][i];
  if(Ans >= p) Ans -= p;
}

70

改变状态的定义, fi,j 表示所有元素小于等于 ji 的方案数, 也就是 50 状态的前缀和.

fi,j=fi,j1+fij,j

这时转移可以理解为, fi,j1 表示的单个元素小于等于 j1, 组成 i 的方案中, 其元素一定小于 j, 所以也应该统计入 fi,j 中.

fij,j 则是将元素在 j 以内的 ij 的划分中, 加入一个元素 j 得到的方案.

这样可以把转移优化到 O(1), 状态仍是 O(n2), 时间复杂度 O(n2).

f[1][1] = 1;
for (register unsigned i(1); i <= n; ++i) {
  f[0][i] = 1;
}
for (register unsigned i(1); i <= n; ++i) {
  for (register unsigned j(1); j <= i; ++j) {
    f[i][j] = f[i][j - 1] + f[i - j][j];
    if(f[i][j] >= p) f[i][j] -= p;
  }
  for (register unsigned j(i + 1); j <= n; ++j) {
    f[i][j] = f[i][j - 1];
  }
  if(f[i][i] >= p) f[i][i] -= p;
}
printf("%u\n", f[n][n]);

80

发现对于第二维只会用到 jj1 可以滚动数组, 只保留第一维, 对整个数组进行 O(n) 次转移.

()fi=()fi+()fij

状态 O(n2), 空间复杂度 O(n), 转移 O(1), 时间复杂度 O(n2).

之所以能多得 10, 是因为时空复杂度同阶的时候, 往往空间更容易炸, 于是优化了空间.

f[0] = 1;
for (register unsigned j(1); j <= n; ++j) {
  for (register unsigned i(j); i <= n; ++i) {
    f[i] += f[i - j];
    if(f[i] >= p) f[i] -= p;
  }
}
printf("%u\n", f[n]);

100

根号分治, 设 Sq=n, 先将所有 jSqfi 求出来, 这时的 fi 表示单个元素不超过 ji 的划分方案数. (除了 j 的上界以外和 80 完全相同)

n = RD(), p = RD(), Sq = sqrt(n) + 1;
f[0] = 1;
for (register unsigned j(1); j <= Sq; ++j) {
  for (register unsigned i(j); i <= n; ++i) {
    f[i] += f[i - j];
    if(f[i] >= p) f[i] -= p;
  }
}

然后再定义一个数组 gi,j, 表示包含 i 个非 0 元素的 j 的划分, 其中, iSq.

gi,j=gi1,j1+gi,ji

转移也很简单 gi1,j1i1 个元素的 j1 的划分, 所有这种划分加上一个元素 1 就是一个 i 个元素, j 的划分. gi,j1i 个元素, ji 的划分, 给这种划分的每个元素加 1, 得到的就是一个最小元素大于 1i 个元素的 j 的划分. 这两种情况互不重复, 因为前者最小元素是 1, 后者最小元素大于 1. 也不会遗漏, 因为任何可行的划分都是可以由 1 的划分和这两种转移转移而来的.

g[0][0] = 1;
for (register unsigned i(1); i <= Sq; ++i) {
  for (register unsigned j(i); j <= n; ++j) {
    g[i][j] = g[i - 1][j - 1] + g[i][j - i];
    if(g[i][j] >= p) g[i][j] -= p;
  }
}

然后统计答案.

我们把一个划分中的元素分成两类: Sq>Sq.

枚举 i 作为 >Sq 的元素个数, 因此 (Sq+1)in, i=O(n).

枚举 j 作为 >Sq 的元素总和, 因此 j[(Sq+1)i,n].

对于每一个 i, j 作为约束, 求合法 n 的划分方案数.

分别讨论两种元素, 首先是 Sq 的元素, 它们的总和应该是 nj, 前面求出了 f 数组, 因此元素不大于 Sqnj 的划分就是 fnj.

接下来是 >Sq 的元素, 因为每个元素都大于 Sq, 所以我们可以只考虑它们比 Sq 多出来的部分. 所以相当于求包含 i 个元素的 jSq×i 的划分, 也就是 gi,jSq×j.

因为两种元素对 jnj 的划分中, 不同种类的元素大小一定不同, 所以互不干扰, 使用乘法原理统计答案即可:

Ans=i=0(Sq+1)in(j=(Sq+1)ijn(fnj×gi,jSq×i))

for (register unsigned i(0); i * (Sq + 1) <= n; ++i) {
  for (register unsigned j(i * (Sq + 1)); j <= n; ++j) {
    Ans = ((unsigned long long)g[i][j - Sq * i] * f[n - j] + Ans) % p;
  }
}

对于 f, 状态数 O(nn), 转移 O(1), 时间复杂度 O(nn).

对于 g, 状态数 O(nn), 转移 O(1), 时间复杂度 O(nn).

对于 Ans, 时间复杂度 O(nn).

总复杂度 O(nn).

posted @   Wild_Donkey  阅读(92)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示