In mathematics you don't understand things, you just get used to them.

标签 dp 下的文章

link。

原问题即:请你给出不同的序列 $\{a_n\}$ 的数量,满足 $0\leqslant a_i<i$,且 $\sum a_i=k$。

那么写出 ${a_n}$ 的 ogf,可得答案为:$\displaystyle [x^k]\left(G(x)=\sum_{i=0}^{n-1} x^i=\frac{1-x^n}{1-x}\right)=\frac{\prod_{i=1}^n(1-x^i)}{(1-x)^n}=\left(\prod_{i=1}^n(1-x^i)\right)\left(\sum_{i=0}^n\binom{n-1+i}{i} x^i\right)$。

前面那个括号是有组合意义的,即有 $n$ 个物品,其第 $i$ 个体积为 $i$,有个容量 $k$ 的背包,求恰好填满背包的方案数,这个方案数还要乘一个系数 $(-1)^m$,$m$ 为选的物品的个数。

后面那个你就直接算,前面的可以 dp。考虑这样一个构造过程:

一开始什么数都没有。对已有的数,我们有两种操作,一种是全部加 $1$,一种是全部加 $1$ 然后再加入一个值为 $1$ 的数。这样执行若干次操作之后构造出来的数列一定满足条件。

https://blog.csdn.net/a_crazy_czy/article/details/72862151

然后就设 $f(i,j)$ 为选了 $i$ 个数,目前的和为 $j$ 的方案数,转移很简单,依照两种操作模拟即可。

太神仙了,我脑子不够用。然后第一维的规模是根号,所以能过了。

#include <bits/stdc++.h>

constexpr int kMod = 1e9 + 7;
constexpr int kN = 1e5, kSqrt = 500;
int n, k, f[kSqrt + 5][kN + 5], fac[kN * 2 + 5], ifac[kN * 2 + 5];

inline void addeq(int& u, const int v) { (u += v) >= kMod && (u -= kMod); }
inline void muleq(int& u, const int v) { u = static_cast<long long>(u) * v % kMod; }
inline void subeq(int& u, const int v) { (u -= v) < 0 && (u += kMod); }
inline int add(int u, const int v) { return (u += v) < kMod ? u : u - kMod; }
inline int mul(const int u, const int v) { return static_cast<long long>(u) * v % kMod; }
inline int mpow(int x, int y) {
    int res = 1;
    for (; y; y >>= 1, muleq(x, x))
        if (y & 1) muleq(res, x);
    return res;
}

template <typename... Args>
inline int mul(const int u, const int v, const Args... args) { return mul(u, mul(v, args...)); }

inline void init(const int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; ++i) fac[i] = mul(fac[i - 1], i);
    ifac[n] = mpow(fac[n], kMod - 2);
    for (int i = n - 1; ~i; --i) ifac[i] = mul(ifac[i + 1], i + 1);
}
inline int com(const int x, const int y) { return mul(fac[x], ifac[x - y], ifac[y]); }

signed main() {
    init(2 * kN);
    
    std::cin >> n >> k;
    f[0][0] = 1;
    const int kS = std::trunc(std::sqrt(k * 2) + 1);
    for (int i = 1; i <= kS; ++i)
        for (int j = 0; j <= k; ++j) {
            if (j >= i) f[i][j] = add(f[i - 1][j - i], f[i][j - i]);
            if (j >= n + 1) subeq(f[i][j], f[i - 1][j - n - 1]);
        }
    
    int res = 0;
    for (int i = 0; i <= kS; ++i)
        for (int j = 0; j <= k; ++j) addeq(res, mul((i & 1) ? kMod - 1 : 1, f[i][j], com(k - j + n - 1, k - j)));
    
    std::cout << res << "\n";
    return 0;
}

发现终于最讨厌的还是和自己的相同的人的样子。

A

傻逼题,不算复杂度差不多得了,显然交换 $S$ / $T$ 的选出区间中的任意位置不影响答案,于是前缀和即可。

B

清新题,只不过我的确不会...部分分设置不太合理。

你考虑用 $O(h ^ 2 w ^ 2)$ 的时间钦定两个点 $(x_1, y_1), (x_2, y_2)$ 研究线段,同时令 $a = |x_1 - x_2|, b = |y_1 - y_2|$。如果你做过 仪仗队 那么很容易想到中间一共有 $\gcd(a, b)$ 个点,把这些点当作盒子,把它们拍到序列上,则我们有 $\gcd(a, b) - 1$ 个盒子,$n - 2$ 个点(钦定端点必选)。如果我们令 $k=\lfloor\frac{d}{{\rm dis}((0, 0), (\frac{a}{\gcd(a, b)}, \frac{b}{\gcd(a, b)}))}\rfloor$,则每个球放进的盒子编号需差 $\geqslant k$。

赋予组合意义,则每个球存在「体积」的概念,且该「体积」为 $k - 1$,考虑将其压缩, 那么答案式子就出来了 $\binom{\gcd(a, b) - 1 - (k - 1) - (n - 2)(k - 1)}{n - 2}$,那个 $k - 1$ 是右端点已经必选了,所以少了 $k - 1$ 个盒子。

考虑优化,注意答案取决于 $a$ 和 $b$,可以乘个系数来优化。

C

清新题,部分分非常暗示。

有个显然的 observation 是一个结点一定从子树中至多选两个未被选择的结点转移,那么就可以直接可并堆启发式合并什么的来转移。

主要问题是为什么不是儿子中选呢?你考虑一个结点 $x$,其父亲 $pre(x)$ 只有 $x$ 一个儿子,而 $x$ 有三个儿子,并且 $x$ 把其中两个儿子选完了,那么 $pre(x)$ 一定是从 $x$ 的另一个儿子转移上来,所以必须把子树考虑完。

D

你出你🐎的 BM 呢😅😅。

不太了解这个东西的具体定义是什么,总之应该是一个用数据结构维护 DP 状态的某几个维度的 trick 吧。

事实上你可以把这篇 post 理解为三个题的解集。

先直接来看 noi2020 - Destiny 这个题。

给定一棵树 $T = (V, E)$ 和点对集合 $\mathcal Q \subseteq V \times V$ ,满足对于所有 $(u, v) \in \mathcal Q$,都有 $u \neq v$,并且 $u$ 是 $v$ 在树 $T$ 上的祖先。其中 $V$ 和 $E$ 分别代表树 $T$ 的结点集和边集。求有多少个不同的函数 $f$ : $E \to \{0, 1\}$(将每条边 $e \in E$ 的 $f(e)$ 值置为 $0$ 或 $1$),满足对于任何 $(u, v) \in \mathcal Q$,都存在 $u$ 到 $v$ 路径上的一条边 $e$ 使得 $f(e) = 1$。由于答案可能非常大,你只需要输出结果对 $998,244,353$(一个素数)取模的结果。

我们略过 DP 的过程,直接给出其定义 $f(x,j)$ 表示考虑子树 $i$,限制条件的 $v\in{\rm subtree}(x)$ 且限制 $(u,v)$ 尚未被满足,$u$ 的深度最深且 $j={\rm dep}(u)$ 的不同映射 $f:E_{{\rm subtree}(x)}\rightarrow\{0,1\}$ 数量,以及其转移$f(x,i)= f(x,i)\sum\limits_{y\in{\rm suf}(x)}\left(\sum\limits_{j=0}^{{\rm dep}(x)}f(y,j)+\sum\limits_{j=0}^{i}f(y,j)\right)+f(y,i)\sum\limits_{j=0}^{i-1}f(x,j)$。

令 $g(x)$ 为 $f(i)$ 的前缀和,得到平方级算法。

如果我们考虑把 DP 的第二维度状态放到线段树上,那么子树的合并就可以放到线段树上去做,即使用线段树的合并 trick 来做 DP。

我们来看看合并的具体过程。贴近实现,我们令 ${\rm merge}:\{(x,y,l,r,s_x,s_y)\}\rightarrow{\rm node}$ 表示线段树合并的过程,其中 $s_x$ & $s_y$ 表示 DP 的前缀和(即 $g$),在实现(以及下文的讲解)中,均把这两个变量视作别名。

有这样几种情形需要探讨。

  • $l=r$:此时应该把 $f(y,l)$ 合并到 $f(x,l)$ 中,直接对线段树结点维护的 DP 值进行修改;
  • $x=\Omega$:此时 $f(x)$ 的 DP 值并没有意义,在本题中可以视作零。于是打乘法标记即可;
  • $y=\Omega$:与上一条类似。

于是得到解决,参考实现。

再来看 pkuwc2018 - Minimax 这个题。

给出一棵有 $n$ 的结点的二叉有根树,并给出其叶子结点的权值,对于一个非叶子结点,其权值有 $p_i$ 概率取得儿子中的最大值, $1-p_i$ 的概率取得最小值。

令 $\{v_{i}\}$ 表示最终根结点($1$-th 结点)的所有可能取值(升序排列),其个数记为 $m$,每一个 $v_i$ 取得的概率记为 $r_i$,将其按照 ${\rm hash}(i)=i\times v_i\times r_i$ 的规则求出 $\sum_i{\rm hash}(i)$。

与上一题类(完 全)似(一 致)地,同样略过 DP 的过程,给出其定义 $f(i,j)$ 表示结点 $i$ 取得值 $j$ 的概率,以及其转移 $f(i,j)=\sum\limits_{v\in{\rm suf}(i)} f(v,j)\left(p_i\sum\limits_{1\leqslant k<j}f(v',k)+(1-p_i)\sum\limits_{j<k}f(v',k)\right)$,其中 $v'$ 表示 ${\rm suf}(i)\setminus\{v\}$ 中的唯一取值。

令 $g(i)$ 为 $f(i)$ 的前缀和(显然这里的 $\infty$ 并不是「真正的无限」),得到平方级的算法。

同样考虑把 DP 的第二维度状态放到线段树上,在线段树合并时维护 $s_x$ & $s_y$ 表示 DP 的前缀和,直接维护即可。

参考实现。

最后来看到 codeforces - 671D / Roads in Yusland

给定一棵 $n$ 个点的以 $1$ 为根的树。有 $m$ 条路径 $(x,y)$,保证 $y$ 是 $x$ 或 $x$ 的祖先,每条路径有一个权值。你要在这些路径中选择若干条路径,使它们能覆盖每条边,同时权值和最小。

这题的平方 DP 都有一定难度……看了 duyi 的题解挺久才理解……不过如果做过 noi2020 - Destiny 的话应该会好很多。

称 ${\rm route}(u,v)$ 中的 $u$ 为起点,$v$ 为终点,在 $v$ 决策一个 route 是否被选择。参考 noi2020 - Destiny 的状态设计,令 $f(x,i)$ 表示在 ${\rm subtree}(x)$ 中选择了若干 routes,其中终点深度最深并且高于 ${\rm dep}(x)$ 的深度为 $i$ 的最优方案。

转移即 $f(x,i)=\min\limits_{y\in{\rm suf}(x)}\{f(y,i)+\sum\limits_{z\in{\rm suf}(x)\setminus\{y\}}g(z)\}$,其中 $g(x)=\min\limits_{1\leqslant i<{\rm dep}(x)}\{f(x,i)\}$,还需要考虑每条 route 带来的额外转移,转移式显然不赘。这些 transforming formulae 也许带有一些构造成分在里面,至少我觉得不太自然……

然后考虑线段树合并优化,你需要支持区间增量 & 全局查询最小值 & 合并(与此同时区间取 $\min$)& 单点取 $\min$。因为 $O((n+m)\log_2n)$ 的空间复杂度并不能通过此题。

考虑以时间换空间,我们采用权值平衡树 & 启发式合并来解决(参考实现中给出的是 std::set)。

平衡树上每个结点维护一个 2-tuple $(i,f(x,i))$,以第一关键字排序。我们依次考虑如何支持上文的操作。

  • 区间增量:如果及时把 $i>{\rm dep}(x)$ 的元素弹出,这就变成了全局增量,直接维护即可;
  • 全局查询最小值:这个是本题最妙的地方,因为关键字的选取,平衡树无法简单地取出最小值,我们需要更多的性质。注意到 $\forall j<k,s.t.f(x,j)<f(x,k)$,此时的 $k$ 都是无用的,可以直接删除,这得出了此题的单调性。如此全局最小值就是平衡树上最右边的结点;
  • 合并:启发式合并即可;
  • 单点取 $\min$:相当于插入操作,直接来即可,需要注意一些不是特别细的细节(雾)。

如此时间复杂度退成了 $O((n+m)\log_2^2n)$,但是空间复杂度优化到了 $O(n+m)$,可以通过此题。

参考实现。

link。

值域分治优化决策单调性 DP 的 trick。朴素做法 trivial,不赘述。

考虑求取一个区间 $[l,r]$ 的 DP 值。先搞定在 $m=\lfloor\frac{l+r}{2}\rfloor$ 的 DP 最优决策点,由于决策的单调性,$[l,m)$ 和 $(m,r]$ 的最优决策点就在 $[l',m']$ 和 $[m',r']$($'$ 系列变量代表最优决策点)。

于是值域分治解决。

#include <bits/stdc++.h>
template <class T> inline void chmax(T& a, const T b) { a = a > b ? a : b; }
template <class T> inline void chmin(T& a, const T b) { a = a < b ? a : b; }
inline long long rd() {
  long long x = 0; bool f = 0; char ch = getchar();
  while (ch < '0' || ch > '9') f |= (ch == '-'), ch = getchar();
  while (ch >= '0' && ch <= '9') x = x * 10 + (ch & 15), ch = getchar();
  return f ? -x : x;
}
template <class T>
constexpr T kInf = std::numeric_limits<T>::max();
int n, k, a[100100]; long long dp[100100][30];
namespace sm {
long long Res = 0; int app[100100], L = 1, R;
inline long long res() { return Res; }
inline long long cal(int x) { return 1ll * x * (x - 1) / 2; }
void prog(int l, int r) {
  auto upd = [&](int p, int d) -> void {
    Res -= cal(app[a[p]]);
    app[a[p]] += d;
    Res += cal(app[a[p]]);
  };
  while (L > l) upd(--L, 1);
  while (R < r) upd(++R, 1);
  while (L < l) upd(L++, -1);
  while (R > r) upd(R--, -1);
}
}  // namespace Sweepline Mo
void Rawgrass(int l, int r, int lg, int rg, int K) {
  if (l > r) return;
  int mid = (l + r) >> 1, pos = 0, rrg = std::min(rg, mid - 1);
  dp[mid][K] = kInf<long long>;
  for (int t = lg; t <= rrg; ++t) {
    sm::prog(t + 1, mid);
    if (dp[t][K - 1] != kInf<long long> && dp[mid][K] > dp[t][K - 1] + sm::res())
      dp[mid][K] = dp[t][K - 1] + sm::res(), pos = t;
  }
  Rawgrass(l, mid - 1, lg, pos, K);
  Rawgrass(mid + 1, r, pos, rg, K);
}
signed main() {
  n = rd(), k = rd();
  for (int i = 1; i <= n; ++i) a[i] = rd();
  for (int i = 1; i <= n; ++i) sm::prog(1, i), dp[i][1] = sm::res();
  for (int i = 2; i <= k; ++i) Rawgrass(1, n, 1, n, i);
  printf("%lld\n", dp[n][k]);
  return 0;
}

对 @command_block 没有 implementation 做法的细化。理论来说可以通过,但因为我实现得较劣无法通过。:(

把金币中的空隙看作石子,就是一个阶梯 Nim 的模型(有总共 $n-m$ 个石子,$m+1$ 个石堆,其中最左边有一个独立的石堆)。于是问题转化为满足偶数编号的石堆石子数异或和非零的初始方案数。

取补集后可以给出这样一个 DP:令 $f(k,i,j,v\in\{0,1\})$ 表示考虑第 $k$ 个二进制位,决策好了 $i$ 个石堆,恰好 $j$ 个石子被分配,当前填 $0$ / $1$ 的方案数。

转移,考虑根据 $k$ 来分层,然后每个层独立地转移,最后来合并。具体来说,$f(k,i,j,v)\rightarrow f(k,i+1,j,v)$,并且 $f(k,i,j,v)\rightarrow f(k,i+1,j+2^k,v\oplus[i+1\equiv0\pmod2])$,即考虑填 $0$ 还是 $1$。令第 $k$ 层合并后的结果为 $s(k,i,j,v)$,最后的答案即 $\binom{n}{m}-s(\lfloor\log_2a_i\rfloor,m+1,n-m,0)$。合并的过程是 $s(k,m+1,j,0)=\sum\limits_i f(k,m+1,i,0)\times s(k-1,m+1,j,0)$。

边界即 $f(k,0,0,0)=1,s(0,m+1,j,0)=f(0,m+1,j,0)$。

时间复杂度 $\Theta(nm\log_2n)$。这是一份比较朴素的实现方法,可以帮助理解,并不能通过此题。

然后你会发现可能由于内存访问不连续等原因,朴素的实现并不能通过此题较大的数据。

考虑交换数组顺序,利用分奇偶讨论转移的特殊性来乘一个 $\frac{1}{2}$ 的常数等方法卡常,有效,但不完全有效。

但是确实过不去,希望哪天来个卡常大师给卡进去。(