4.17 CW 模拟赛 T1. 稻田灌溉
前言
做什么事一定要静下心来
思路
首先考虑 \(m = 2, m = 3\) 时的情况
显然不支持再扫一遍处理
因此需要考虑一些性质
首先考虑枚举前 \(m - 1\) 次区间, 然后最后一次可以将点分为
- 必须被覆盖
- 必须不被覆盖
- 随意
分讨的复杂度显然是 \(\mathcal{O} (n)\) 的
最终我们需要一遍 \(\mathcal{O} (n)\) 处理包含所有必须被覆盖的点, 不包含必须不被覆盖的点的区间个数
首先发现区间难以考虑, 最初所想的要么新开, 要么继承直接做区间 \(\rm{dp}\) 是不好处理的
但是找点性质不难处理成 \(\rm{dp}\) 结合约束处理当前方案数, 因为前后是不相干的
注意到位置 \(i\) 被浇灌的次数 \(= L_i - R_i\), 其中 \(L_i, R_i\) 分别表示 \(i\) 及之前的左端点个数和右端点个数, 这是显然的
那么根据位置 \(i\) 放 \(\alpha\) 个左端点, \(\beta\) 个右端点, 我们可以结合之前的左右端点个数处理出这时的答案
这也是经典的区间限制问题的转化, 应该是见过的但是忘了在哪了
主要是不这样转化真的没啥其他的方法了, 要是说差分的话太难处理了, 于是就先这样, 也不深究为什么这么搞了, 你赛时都想到了还在这纠结啥啊
考虑简单的结合约束
\(f_{i, l, r}\) 表示考虑前 \(i\) 个点, 左端点个数为 \(l\), 右端点个数为 \(r\) 的方案数, 显然要求 \(r \leq l \leq m\)
考虑如何转移
我们显然可以枚举当前点 \(i\) 放多少个左端点, 多少个右端点
然后考虑转移
在此之前需要考虑一下最终答案如何处理, 是否能找到一个合理的表达式
考虑 \(L, R\) 的放置方式对应的答案, 设 \(\alpha_i, \beta_i\) 分别表示 \(i\) 放多少个左端点, 多少个右端点
发现最终不仅要计算配对数, 完了还要计算每种配对的可重集排列
更深刻的理解
问题
对于每个点, 记 分别表示这个位置上的左右端点个数 保证 , 因为不这样显然无解
求所有可能的左右端点配对方案的可重集排列数之和
具体的, 假设最终配对出来的可重区间集合 , 要求这个集合的可重集排列
是否存在一个柿子可以快速计算上述方案数
解决方法
实际上这样是不行的, 堵死了我们用最终的形式去处理的方法
我们考虑另一种方法, 考虑边处理边计算
注意因为不好从柿子的角度考虑, 我们可能需要找到「可重集排列」的性质然后在 时直接处理
可重集的排列有什么性质呢?
例如说对于集合 , 那么 和 实际上是等价的
在本题中, 对于相同位置的右端点, 如果恰好选到了相同位置的左端点, 那么显然会算重一些
因此不能使用配对方案数直接做排列
考虑怎么解决
我们先考虑直接计算配对方案数该怎么做, 然后再考虑去重
计算配对方案数是简单的, 只需要对每个位置的右端点去找左端点匹配即可, 比较经典且在 中也有记载
于是考虑去重
一种经典的去重方法是考虑容斥, 显然跟这题没关系, 因为也不是交集并集
另外一种方法新开维护也不好处理
于是不妨在 时钦定顺序以此去重
考虑对每个左端点赋上一个编号, 最终钦定按照编号顺序排列
这样就可以保证不会算重
具体的, 我们考虑对每个左端点 钦定一个编号 , 然后按照 从小到大对区间排序
并且对于一个位置的右端点, 我们不关注右端点内部的顺序事实上直接算配对方案时好像也不用关注相同位置右端点内部的顺序, 直接为他选择之前的未配对左端点, 与之前不同的是, 这次选择的左端点是定序的, 也就是说只要选了那几个 就一定被放置了
需要注意的是这样处理的话, 每个位置放左端点的时候, 需要用一个组合数表示选择哪些序号
本质上来讲, 是通过将左端点定序来去掉排列算重的影响
其他的右端点配对实际上是一样的
这个时候我们尝试回到最初的问题, 是否可能用数学柿子表示, 现在看来是可能的
不妨记 表示这个位置上的左右端点个数 保证 , 因为不这样显然无解
记 的前缀和分别为
最终的答案应该为
这是为什么?
不难发现可以拆成两个部分
其中 部分显然是配对方案数, 相同位置的 不区分顺序
考虑 部分
不难发现就是一个对 定序的过程, 其中相同位置的 不区分顺序, 考虑证明每种 定序之后对应的配对方案数之和就是多重集排列数
实际上比较显然, 因为相同位置的 不区分顺序, 相同的 不再会算重, 而且通过这个也成功地算全了
这是一个怎么样的思路呢?
相同的 会算重 能不能统计相同 的个数 实则不行 考虑对每个位置上的 钦定在最终的序列中的顺序 这样就可以去掉排列算重的影响 成功
好像说多重集排列的另外一种理解方式不就是
将所有组合数相乘时, 分子中的 会与分母中的 逐项抵消, 最终仅保留首项分子 和末项分母 , 而分母中的每个 则保留下来
因为 , 则末项分母为 , 表达式简化为:
显然就是多重集排列的柿子
因此 部分可以直接用多重集排列的柿子来表示
总的来讲, 这个问题其实并没有最初所想的那样复杂
本质上就是一个对左右端点的配对方案数乘上一个左端点的多重集排列
具体是为什么呢?
你发现加上配对方案数之后, 区间只和左端点相关, 于是拼上一个左端点的可重集排列即可
这也是可重集排列的一种理解, 也就是对每种物品内部不区分去做位置的选取
但是发现上面的理解仍然错误
其中 部分确实是关于 的可重集排列, 表示 所有可能的顺序
然后 部分在每种 顺序的基础上, 通过不区分相同位置的 来计算整个区间集合的可重集排列数
在总结一下, 想要计算区间集合的可重集排列数, 不妨先考虑左端点的可重集排列数, 再把右端点拼上去
最大的问题应该是咋想到的?
最终的区间可重集排列可以拆分成 的排列和 的排列
显然对 分别计算可重集排列在本题中是不行的, 因为 对位置有约束, 因此考虑带着约束拼上去
可以尝试这样思考
是否可以看做 分别做可重集排列 确实是, 但是有些是非法的 如何只考虑合法情况 考虑使用挂载法
注意还有当前点的约束, 懒得打了
然后你发现直接转移是 \(\mathcal{O} (nm^4)\) 的, 考虑优化
不难发现上述过程中 \(\alpha, \beta\) 可以拆开
再具体一点, 考虑固定 \(l, r\) 后对于 \(\alpha_1, \alpha_2\) 和一个 \(\beta\) 的转移
注意这里出现了无用的转移:
\((1), (2)\) 两个柿子, 完全可以拆成
考虑每次转移先设辅助数组 \(g_{l + \alpha, r}\) 转移
然后再利用 \(g\) 转移 \(f\) 即可, 显然可以优化到 \(\mathcal{O} (nm^3)\), 卡常可以通过
注意先转移 \(g\) 的时候要考虑当前点约束, 最后再转移 \(f\) 的时候就不用了, 因为当前放多少个右端点不影响当前点约束
参考代码(状态设计略有不同, 仅供参考)
#include "bits/stdc++.h"
#pragma GCC target("avx2")
#define int long long
const int MAXN = 3e2 + 10, MOD = 998244353;
namespace calc {
int add(int x, int y) { return x + y >= MOD ? x + y - MOD : x + y; }
int sub(int x, int y) { return x - y < 0 ? x - y + MOD : x - y; }
int mul(int x, int y) { return 1LL * x * y % MOD; }
void addon(int &x, int y) { x = add(x, y); }
void subon(int &x, int y) { x = sub(x, y); }
void mulon(int &x, int y) { x = mul(x, y); }
} using namespace calc;
int n, m, a[MAXN], b[MAXN];
int f[MAXN][MAXN][MAXN], g[MAXN][MAXN][MAXN], C[MAXN][MAXN];
signed main() {
scanf("%lld %lld", &n, &m);
for (int i = 1; i <= n; i++) scanf("%lld", a + i);
for (int i = 1; i <= n; i++) scanf("%lld", b + i);
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= i; j++) {
if (j == 0 || i == 0) C[j][i] = 1;
else C[j][i] = add(C[j][i - 1], C[j - 1][i - 1]);
}
}
f[0][0][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= b[i + 1]; j++) {
for (int k = 0; k + j <= m && m - k >= a[i + 1]; k++) {
if (f[i][j][k]) {
#pragma GCC unroll 8
for (int l = (a[i + 1] > j ? a[i + 1] - j : 0); l + j + k <= m && l <= b[i + 1] - j; l++) addon(g[i + 1][j + l][k], mul(f[i][j][k], C[l][m - j - k]));
}
}
}
for (int j = a[i + 1]; j <= b[i + 1]; j++) {
for (int k = 0; j + k <= m; k++) {
if (g[i + 1][j][k]) {
#pragma GCC unroll 8
for (int l = 0; l <= j; l++) addon(f[i + 1][j - l][k + l], mul(g[i + 1][j][k], C[l][j]));
}
}
}
}
printf("%lld\n", f[n][0][m]);
return 0;
}
总结
合法区间个数的一种处理方式: 想办法处理出两端点位置的限制, 然后组合数学处理
区间覆盖问题的经典转化
这种带约束计算方案数的问题, 往往在处理同时判定合法性
通过列出表达式来简化转移的思考
左右端点的配对问题是经典的
但是注意本题中最关键的问题: 如何去除相同区间的影响
注意到其等价于: 固定 \(l\) 顺序后\((\)多重集排列\()\), 在基础上, 通过不区分相同位置的 \(r\) 来计算整个区间集合的可重集排列数
本质上是要去除 \(l, r\) 都相同的排列, 不妨确定 \(l\) 顺序之后把 \(r\) 挂上去
也就是说, 倘若遇到可重集相关的组合问题, 可以考虑对同类型物品合并做选取
组合数学中是否区分顺序对结果的意义影响很大
多状态 \(\rm{dp}\) 的一种优化方式:
拆分转移柿子中不同状态的影响, 然后分开转移
一般利用辅助数组先转移一部分, 然后再乘上一些系数
附: 更深的理解原文

浙公网安备 33010602011771号