7 条题解
-
0
子任务 5:用第二次筛选处理双固定列(通过子任务 1–6,共 80 分)
双固定列不需要增加状态维数
子任务 5 满足 ,第一列和最后一列都有预置士兵,但同一列现在可能有两个预置士兵。仍把至少有一个预置士兵的列称为关键列,依次位于 。
每个关键列选一个预置格作为固定端:有上格时选上格,否则选下格。其所在行为 ,兵种为 。另一个格子的预置兵种记为 ,没有预置时令 。设 为填好前 列、满足这些列全部预置条件、且当前关键列另一格兵种恰好为 的方案数。
当两格都已固定时,只需在这张一维表里保留 ,而不是再增加一维枚举固定的兵种。因此可以先解决“给定右端一格”的转移,再用另一个预置格进行筛选。
空白段中保留什么信息
考虑从第 个关键列到第 个关键列,令 ,右端固定端的行为 、兵种为 。在本段始终以行 为第一个兵种坐标,另一行为第二个坐标。
设 表示填好前 列、当前两坐标兵种恰好为 的方案数,。左端及以前的预置条件已施加,右端的两个预置条件都暂不施加。对于同列相同的兵种,规定 。
定义
$$\begin{aligned} S(t)&=\sum_{x=1}^{m}\sum_{y=1}^{m}F(t,x,y),\\ R(t,c)&=\sum_{y=1}^{m}F(t,c,y),\qquad C(t,c)=\sum_{x=1}^{m}F(t,x,c),\\ H(t,c)&=F(t,q,c). \end{aligned}$$是目标兵种对应的一行状态,称为目标截面;它不是全部方案的分布。中间列的第一个坐标可以取任意兵种,转移依靠 保留这些方案的信息。
新列两兵种 分别需要避开同一物理行的前驱兵种,容斥得到
对所有 求和,其中 ,于是
$$\begin{aligned} R(t+1,x) &=(m-1)(S(t)-R(t,x))-(S(t)-C(t,x))+R(t,x)\\ &=(m-2)(S(t)-R(t,x))+C(t,x). \end{aligned}$$对称地,
每个合法旧列扩展一列空白时,两格分别避开旧兵种有 种,再去掉两格同色的 种,故记
则 。截面直接满足
$$H(t+1,c)= \begin{cases} S(t)-R(t,q)-C(t,c)+H(t,c),&c\ne q,\\ 0,&c=q. \end{cases}$$所有方程对 取模,负数归一化。到达右端以前,不应对 之外的全部状态施加兵种 ,否则会把整段误当作每列都预置了 。
左端的两种方向与右端的第二次筛选
设左端固定兵种为 、行为 ,,。使用指示值 。
若 ,左端状态只有 的一行非零,因此
$$S(0)=w,\quad R(0,c)=[c=p]w,\quad C(0,c)=f(c),\quad H(0,c)=[p=q]f(c).$$若 ,左端固定兵种位于第二个坐标,,因此
$$S(0)=w,\quad R(0,c)=f(c),\quad C(0,c)=[c=p]w,\quad H(0,c)=[c=p]f(q).$$即使左端两格都固定,以上公式仍然成立,因为此时 本来就至多有一个非零项,无须新增分支。
递推到 后,先以 保证右端固定端的兵种是 。右端另一格预置兵种为 时,再令
$$f(j+1,c)= \begin{cases} H(d,c),&b_{j+1}=0\text{ 或 }c=b_{j+1},\\ 0,&\text{其他情况}. \end{cases}$$若右端两个预置兵种相同,即 ,唯一可能保留的 本来就是零,因而自然得到无解。不能把双固定列直接当成“贡献乘一”:它还会限制和左侧相邻格的兵种关系。
例如 ,左列上下固定为 ,右列上格固定为 。施加右下格条件前,下格取 各有一种方案;取 与左下格相同,取 与右上格相同,都为零。若右下格再固定为 ,保留的方案数为 ;固定为 或 时,答案为 。第二次筛选保留的是计算出的对应项,而不是一个无条件的常数。
下一段必须从筛选后的 重算总和与行列和。实现中本段最后一步只计算截面,不计算随后会被重新初始化的其他统计量。
初始化、两端空白与答案
对子任务 5,第一列即关键列。记其固定端兵种为 ,另一格预置为 ,初始条件为
最后一列也是关键列,答案为 。特别是 时,这个初值已经包含同列冲突检查,直接求和即可。
代码保留一个自然的边界推广:若最左关键列不在第一列,则初值额外乘 ;若最右关键列不在最后一列,则最终答案乘 。这是因为指定任一合法端点列后,向外增加一列空白总有 种扩展,与具体兵种无关。完全没有关键列时,答案为 。线性预处理 的各次幂即可。
这一边界推广使代码也适用于子任务 1、2、6:这些组不保证首尾非空,但 仍不超过 。连同对子任务 3、4 的包含关系,本实现自然覆盖子任务 1 至 6;第 7 组的最坏时间开销仍过大。
完整列状态的容斥转移正确检查全部相邻边;封闭统计量只是精确地求和或取截面;右端的两次筛选分别施加两个格子的预置条件。每段之后得到的都是完整的关键列分布,由此归纳可知最终计数正确。
复杂度
时间复杂度为 。各段逐列推进的距离总和不超过 ,每步扫描全部兵种;关键列方向切换和双固定筛选也只需 ,共至多 次。幂表预处理为 。
空间复杂度为 。完整保存各处理位置的 最坏需要约 个整数,在 时约为 GB,超过 MB,故只维护旧、新两层。每次先计算完整新层再交换,固定列分布就是筛选后的截面,不重复保存历史关键列状态。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; int norm(i64 x) { return (x % mod + mod) % mod; } void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<int> pos; for (int i = 0; i < n; i++) { if (a[i] || b[i]) pos.push_back(i); } int ways = norm(1LL * m * m - 3LL * m + 3); vector<int> pw(n + 1, 1); for (int i = 1; i <= n; i++) pw[i] = 1LL * pw[i - 1] * ways % mod; if (pos.empty()) { cout << 1LL * m * (m - 1) % mod * pw[n - 1] % mod << '\n'; return; } int last = pos[0], side = a[last] ? 0 : 1; int p = side == 0 ? a[last] : b[last]; vector<int> dp(m + 1, pw[last]), ndp(m + 1); vector<int> row(m + 1), col(m + 1); vector<int> next_row(m + 1), next_col(m + 1); dp[0] = dp[p] = 0; if (a[last] && b[last]) { int value = dp[b[last]]; fill(dp.begin(), dp.end(), 0); dp[b[last]] = value; } for (int j = 1; j < (int)pos.size(); j++) { int r = pos[j], next_side = a[r] ? 0 : 1; int q = next_side == 0 ? a[r] : b[r]; int sum = accumulate(dp.begin(), dp.end(), 0LL) % mod; fill(row.begin(), row.end(), 0); fill(col.begin(), col.end(), 0); if (side == next_side) { row[p] = sum; col = dp; if (p != q) fill(dp.begin(), dp.end(), 0); } else { row = dp; col[p] = sum; int value = dp[q]; fill(dp.begin(), dp.end(), 0); dp[p] = value; } for (int i = last + 1; i <= r; i++) { i64 base = 1LL * sum - row[q]; for (int x = 1; x <= m; x++) { ndp[x] = x == q ? 0 : norm(base - col[x] + dp[x]); } if (i < r) { for (int x = 1; x <= m; x++) { next_row[x] = norm(1LL * (m - 2) * (sum - row[x]) + col[x]); next_col[x] = norm(1LL * (m - 2) * (sum - col[x]) + row[x]); } row.swap(next_row); col.swap(next_col); sum = 1LL * sum * ways % mod; } dp.swap(ndp); } if (a[r] && b[r]) { int value = dp[b[r]]; fill(dp.begin(), dp.end(), 0); dp[b[r]] = value; } last = r; side = next_side; p = q; } int ans = accumulate(dp.begin(), dp.end(), 0LL) % mod; cout << 1LL * ans * pw[n - 1 - last] % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
压缩空白区间并枚举关键列兵种(通过子任务 1–6,共 80 分)
从逐列状态到关键列
只看最后一列,就能判断下一列新增的相邻关系。设 为前 列已经填好、且第 列上下兵种分别为 的方案数,其中 。若 ,或者二者不符合该列已驻扎的士兵,状态为零。第一列每个合法状态均为 ,其余列满足
$$F(i,x,y)=\sum_{\substack{1\le u,v\le m\\u\ne x,\ v\ne y}}F(i-1,u,v).$$旧状态已经保证 ,因此不必在求和中重复检查。记旧层总和为 、第 行之和为 、第 列之和为 ,容斥得到
这把一次转移降至常数时间,但每列仍有 个状态,总成本为 。
问题出在完全空白的列。如果某一列已有至少一个士兵,就选一个已知兵种作为这列的固定端:优先选上格,否则选下格。固定端的位置和兵种已知,只需记录另一格的兵种,共 种可能。
把这些列称为关键列,按顺序记位置为 ,位置从 开始。设 为填好直到第 个关键列、该列固定端取已知兵种、另一格取 的方案数。关键列之间的内部列全为空,我们先解决如何一次跨过它们。
用七类关系描述空白区间
固定左端列的上、下兵种为不同的 。两个端点列的下标相差 ,所以中间有 列; 表示同一列。
对于一组指定的右端兵种 ,区间方案数只取决于它们与 的相等关系。原因是:除 外,其他兵种可整体重命名,重命名给出合法方案的一一对应。
设 为相应类别中一个指定右端有序对的方案数。它不是这一类所有右端对的总和。七种类别如下, 表示不等于 的兵种,最后一类还要求 。
右端上下兵种 同类有序对的数量 同列相同兵种非法,故没有其他类别。初始条件为 ,其余 。
预处理下一距离时,仍使用上一节的容斥。为简洁,下面的 表示 。旧层总和是
对某个指定兵种求行和、列和,有
$$\begin{aligned} R_A&=h_0+(m-2)h_2,& C_A&=h_1+(m-2)h_3,\\ R_B&=h_1+(m-2)h_4,& C_B&=h_0+(m-2)h_5,\\ R_O&=h_3+h_5+(m-3)h_6,& C_O&=h_2+h_4+(m-3)h_6. \end{aligned}$$例如,上格已经是指定的 ,下格可以为 、,或者除 外的 种兵种。这里再次体现了“一个指定有序对”与“整类总和”的区别。
若类别 的右端对为 ,则
对每个 计算七个值,预处理为线性时间。于是,对于具体端点 、,比较兵种相等关系即可在常数时间取得区间权重 ;任一端点两兵种相同,权重为零。
关键列之间的求和为什么能变成一次函数
考虑相邻两个关键列,旧固定兵种为 ,新固定兵种为 。旧另一格的兵种记为 ,新另一格记为 。设 为按两列实际上下位置排列端点后得到的 。若固定端从上格变成下格,要交换对应端点对的顺序,不能只比较两个固定兵种。
令 ,则尚未筛选新列另一格的预置限制时,完整转移为
旧列已经保证 ,新列必须令 。
先考虑普通兵种 。在所有有贡献的 中,只须区分:、、以及其余兵种。对最后一类,端点相等关系完全一致,所以权重相同。
选一个不属于 的代表 。题目保证 ,这样的代表总能找到。令
则
$$\begin{aligned} \widetilde f(j,z) &=V W(q,z)+f(z)W(z,z)+(S-V-f(z))H\\ &=\alpha f(z)+\beta,\\ \alpha&=W(z,z)-H,\\ \beta&=S H+V\bigl(W(q,z)-H\bigr). \end{aligned}$$对于所有普通 ,三个权重所代表的相等关系都相同,因此 也相同。只取一个普通代表就能计算这两个系数。若 ,则 ,相关退化项自然消失。
剩下的新兵种 需要单独计算。当 时,旧 只分为 和其余有效兵种:
当 时该值就是零。因此每次关键列转移只需:对所有兵种统一做 ,再修正兵种 和 两处。
例如 ,两列的上格分别固定为 ,下格未定。第一列的状态为 ,其余为 ,故 。对于新下格 ,旧下格只须排除 ,得到 ;新下格取 时,旧下格原本就不能取 ,故有 种;取 时非法。答案为 。这展示了统一更新和两个例外分别承担的作用。
若新列上下两格都已固定,仍按上述方式转移,随后只保留另一格的指定兵种,其他状态全部清零。若上下固定兵种相同,被保留的值本来就是零。
显式保存每个关键列的状态
对子任务 1 到 6, 都不超过 。可以保存完整的 :每一步先遍历旧层求 ,再对所有 显式执行一次函数,修正两个特殊兵种,最后应用双固定列筛选。这里完全使用上一节的方程,只是不进一步优化全体状态的修改。
当固定兵种或固定端的上下位置改变时,重新根据端点关系查询权重。某个乘数为零时,显式赋值仍然正确,也不需要求逆元或额外分类。
两端空白列与最终答案
设某一列已指定不同的上下兵种。与它相邻的一列全部空白时,上下兵种需要分别避开它们,并且彼此不同。先分别选择得到 种,其中两格相同且不等于原来两种兵种的情况有 种,所以扩展数为
这个数与指定的具体兵种无关。因此,最左关键列在位置 时,初始状态为
$$f(1,c)= \begin{cases} D^{\ell_1},&c\ne p_1\text{ 且符合该列另一格的预置限制},\\ 0,&\text{其他情况}. \end{cases}$$依次转移所有关键列后,右边剩余 列也逐列贡献 ,答案为
$$\left(\sum_{c=1}^{m}f(k,c)\right)D^{n-1-\ell_k}\pmod P.$$若完全没有关键列,第一列有 种选择,其余每列贡献 ,答案直接为 。 时上述公式也成立。预先保存 即可。
逐列容斥保证了区间权重的正确性,七类合并来自兵种重命名的一一对应,关键列递推枚举了全部端点兵种且没有重复。显式逐兵种更新严格执行该递推;再结合两端的独立扩展,覆盖了完整网格的所有合法方案。
复杂度
时间复杂度为 :七类空段权重和幂表用 预处理,每个关键列统计并填写 个状态。空间复杂度为 ,完整保留关键列状态; 时约有 个整数,约 400 MB,连同输入和辅助表可放入题目的 512 MB。
该方法不依赖“只在第一行预置”“每列至多一个预置”或“首尾列有预置”,所以自然覆盖子任务 1 至 6。第 7 组的完整状态可能需要约 40 GB,时间成本也会随 增长。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; int norm(i64 x) { return (x % mod + mod) % mod; } void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<int> pos; for (int i = 0; i < n; i++) { if (a[i] || b[i]) pos.push_back(i); } const int ways = norm(1LL * m * m - 3LL * m + 3); vector<int> pw(n + 1, 1); for (int i = 1; i <= n; i++) pw[i] = 1LL * pw[i - 1] * ways % mod; if (pos.empty()) { cout << 1LL * m * (m - 1) % mod * pw[n - 1] % mod << '\n'; return; } // Relative to fixed left colors A,B: AB,BA,AO,OA,BO,OB,OO'. const vector<pair<int, int>> type = {{0, 1}, {1, 0}, {0, 2}, {2, 0}, {1, 2}, {2, 1}, {2, 2}}; vector<array<int, 7>> dp_gap(n); dp_gap[0][0] = 1; for (int d = 1; d < n; d++) { const auto& pre = dp_gap[d - 1]; i64 sum = norm(pre[0] + 1LL * pre[1] + 1LL * (m - 2) * (0LL + pre[2] + pre[3] + pre[4] + pre[5]) + 1LL * (m - 2) * (m - 3) % mod * pre[6]); vector<int> row = {norm(pre[0] + 1LL * (m - 2) * pre[2]), norm(pre[1] + 1LL * (m - 2) * pre[4]), norm(pre[3] + 1LL * pre[5] + 1LL * (m - 3) * pre[6])}; vector<int> col = {norm(pre[1] + 1LL * (m - 2) * pre[3]), norm(pre[0] + 1LL * (m - 2) * pre[5]), norm(pre[2] + 1LL * pre[4] + 1LL * (m - 3) * pre[6])}; for (int t = 0; t < 7; t++) { auto [x, y] = type[t]; dp_gap[d][t] = norm(sum - row[x] - col[y] + pre[t]); } } auto kernel = [&](int d, int x, int y, int u, int v) { if (x == y || u == v) return 0; int t; if (u == x) t = v == y ? 0 : 2; else if (u == y) t = v == x ? 1 : 4; else if (v == x) t = 3; else if (v == y) t = 5; else t = 6; return dp_gap[d][t]; }; vector<vector<int>> dp(pos.size(), vector<int>(m + 1)); int last = pos[0], side = a[last] ? 0 : 1; int p = side == 0 ? a[last] : b[last]; fill(dp[0].begin() + 1, dp[0].end(), pw[last]); dp[0][p] = 0; if (a[last] && b[last]) { int value = dp[0][b[last]]; fill(dp[0].begin(), dp[0].end(), 0); dp[0][b[last]] = value; } for (int j = 1; j < (int)pos.size(); j++) { int i = pos[j], next_side = a[i] ? 0 : 1; int q = next_side == 0 ? a[i] : b[i]; int d = i - last; auto weight = [&](int y, int z) { int x = p, u = q; if (side) swap(x, y); if (next_side) swap(u, z); return kernel(d, x, y, u, z); }; int z = 1; while (z == p || z == q) z++; int w = 1; while (w == p || w == q || w == z) w++; int sum = accumulate(dp[j - 1].begin(), dp[j - 1].end(), 0LL) % mod; int value = dp[j - 1][q]; int other = weight(w, z); int mul = norm(1LL * weight(z, z) - other); int add = norm(1LL * sum * other + 1LL * value * (weight(q, z) - other)); int special = p == q ? 0 : norm(1LL * value * weight(q, p) + 1LL * norm(1LL * sum - value) * weight(w, p)); for (int x = 1; x <= m; x++) dp[j][x] = (1LL * mul * dp[j - 1][x] + add) % mod; dp[j][p] = special; dp[j][q] = 0; if (a[i] && b[i]) { int keep = dp[j][b[i]]; fill(dp[j].begin(), dp[j].end(), 0); dp[j][b[i]] = keep; } last = i; side = next_side; p = q; } int sum = accumulate(dp.back().begin(), dp.back().end(), 0LL) % mod; cout << 1LL * sum * pw[n - 1 - last] % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
子任务 4:切换固定行并维护目标截面(通过子任务 3–4,共 40 分)
每列至多一个预置士兵意味着什么
子任务 4 满足 ,每列至多有一个预置士兵,第一列和最后一列都非空。预置士兵可以位于任意一行,因此不能只考虑上格固定的情形。
把非空列按位置记为 。第 个关键列的固定兵种为 ,所在物理行为 ,另一格未固定。设 为填好前 列并满足全部预置条件、且该关键列另一格兵种恰好为 的方案数,。同列不能同色,所以 。
关键列之间全为空,完整状态需要记录上下两个兵种,但只在右端恢复一个固定兵种对应的截面即可。新增的问题是:相邻关键列的预置士兵可能换行。解决办法不是把输入中的上下行随意互换,而是在处理每一段时,以右端固定兵种所在行为第一个坐标,另一行为第二个坐标。
按右端方向定义列状态
考虑从 到 的一段,长度差为 ,右端兵种为 、所在行为 。本段的 表示填好前 列的方案数,其中行 的兵种为 ,另一行的兵种为 。左端及以前的预置条件已经施加,右端条件暂不施加。。
定义总和、行列和与目标截面:
$$\begin{aligned} S(t)&=\sum_{x=1}^{m}\sum_{y=1}^{m}F(t,x,y),\\ R(t,c)&=\sum_{y=1}^{m}F(t,c,y),\qquad C(t,c)=\sum_{x=1}^{m}F(t,x,c),\\ H(t,c)&=F(t,q,c). \end{aligned}$$这里的矩阵行、列对应兵种坐标,不是网格的物理列。 只是一份被跟踪的截面,中间列并没有被强制固定为 ;转移中的总和必须使用 ,不能使用 。
不管当前选择哪一行为第一个坐标,水平相邻格分别要求第一个坐标不等、第二个坐标不等。因此对 ,完整转移始终是
这是从全部前驱中减去两类水平冲突,再加回被重复减去的交集。对 求和,得到
$$\begin{aligned} R(t+1,x) &=(m-1)(S(t)-R(t,x))-(S(t)-C(t,x))+R(t,x)\\ &=(m-2)(S(t)-R(t,x))+C(t,x). \end{aligned}$$由对称性,另有
每个合法旧列扩展一列空白的方案数都等于
因为先分别避开左右水平冲突,再排除两格同色的 种选择。因此 ,并且
$$H(t+1,c)= \begin{cases} S(t)-R(t,q)-C(t,c)+H(t,c),&c\ne q,\\ 0,&c=q. \end{cases}$$三个长度为 的向量和总和构成封闭递推,所有等式在模 下计算。
同行与换行两种初始化
设左端固定兵种为 、所在行为 ,左端另一格的分布为 ,令 。用 表示命题 的指示值。
当 时,左端只有第一个坐标 的状态有效,故
$$S(0)=w,\qquad R(0,c)=[c=p]w,\qquad C(0,c)=f(c),\qquad H(0,c)=[p=q]f(c).$$当 时,左端的固定兵种位于第二个坐标,状态矩阵只有第 列有效,即 。因此
$$S(0)=w,\qquad R(0,c)=f(c),\qquad C(0,c)=[c=p]w,\qquad H(0,c)=[c=p]f(q).$$两种情况只改变段首初始化,空白列的递推完全相同。特别是换行时,截面不一定全零:它在第二个坐标等于 的位置保留 。漏掉这个值,就丢掉了左端未固定格已经取 的合法前驱。
例如 ,左列上格固定为 ,右列下格固定为 。按右端方向,以“下、上”为两个坐标。左端有 ,,,而 。经过一次转移,新上格取 时得到 ;取 时同列冲突,仍为零;取 时各有 种,共 种。这同时检查了水平冲突和上下坐标的对应。
段末筛选、两端与正确性
递推 次后,右端所在行必须取 ,直接令
下一段根据新的固定行与兵种重新构造 ,其中总和重新求 。不施加右端限制时的 不能直接传到下一段。代码在每段最后一步仅更新截面,因为此时其余统计量已经不再使用。
首列只有一个预置兵种 ,所以 ;末列也是关键列,答案为 。 时直接得到 。
代码也处理每列至多一个预置、但首尾为空的情形:固定合法端点向空白方向扩展一列总有 种选择,故初值乘 ,答案乘 。全空网格的答案为 。这些幂用线性预处理求出。
交换两个物理行只改变坐标名称,不改变任何相邻关系。两种初始化分别精确表达左端是矩阵中的一行还是一列;封闭递推是完整状态递推的精确投影,段末筛选恰好保留右端预置条件。因此每种合法方案被计入一次,非法方案不被计入。
本方法依赖每列至多一个预置士兵,适用于子任务 3、4;子任务 1、2 虽然规模更小,但没有这一结构保证,不能仅凭规模把它们计入覆盖范围。
复杂度
时间复杂度为 。空白段及其右端逐列推进,每个位置扫描全部兵种,所有段的距离之和至多为 ;各关键列的初始化总计 ,其中 。
空间复杂度为 。完整保存各位置的 最坏约需 个整数,即约 GB,超过 MB,所以采用旧、新两层。一次更新先从旧层算出全部新值再交换,右端筛选后的截面同时作为新的关键列分布,不额外保留重复历史。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; int norm(i64 x) { return (x % mod + mod) % mod; } void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<int> pos; for (int i = 0; i < n; i++) { if (a[i] || b[i]) pos.push_back(i); } int ways = norm(1LL * m * m - 3LL * m + 3); vector<int> pw(n + 1, 1); for (int i = 1; i <= n; i++) pw[i] = 1LL * pw[i - 1] * ways % mod; if (pos.empty()) { cout << 1LL * m * (m - 1) % mod * pw[n - 1] % mod << '\n'; return; } int last = pos[0], side = a[last] ? 0 : 1; int p = side == 0 ? a[last] : b[last]; vector<int> dp(m + 1, pw[last]), ndp(m + 1); vector<int> row(m + 1), col(m + 1); vector<int> next_row(m + 1), next_col(m + 1); dp[0] = dp[p] = 0; for (int j = 1; j < (int)pos.size(); j++) { int r = pos[j], next_side = a[r] ? 0 : 1; int q = next_side == 0 ? a[r] : b[r]; int sum = accumulate(dp.begin(), dp.end(), 0LL) % mod; fill(row.begin(), row.end(), 0); fill(col.begin(), col.end(), 0); if (side == next_side) { row[p] = sum; col = dp; if (p != q) fill(dp.begin(), dp.end(), 0); } else { row = dp; col[p] = sum; int value = dp[q]; fill(dp.begin(), dp.end(), 0); dp[p] = value; } for (int i = last + 1; i <= r; i++) { i64 base = 1LL * sum - row[q]; for (int x = 1; x <= m; x++) { ndp[x] = x == q ? 0 : norm(base - col[x] + dp[x]); } if (i < r) { for (int x = 1; x <= m; x++) { next_row[x] = norm(1LL * (m - 2) * (sum - row[x]) + col[x]); next_col[x] = norm(1LL * (m - 2) * (sum - col[x]) + row[x]); } row.swap(next_row); col.swap(next_col); sum = 1LL * sum * ways % mod; } dp.swap(ndp); } last = r; side = next_side; p = q; } int ans = accumulate(dp.begin(), dp.end(), 0LL) % mod; cout << 1LL * ans * pw[n - 1 - last] % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
满分解法(100 分)
从逐列状态到关键列
只看最后一列,就能判断下一列新增的相邻关系。设 为前 列已经填好、且第 列上下兵种分别为 的方案数,其中 。若 ,或者二者不符合该列已驻扎的士兵,状态为零。第一列每个合法状态均为 ,其余列满足
$$F(i,x,y)=\sum_{\substack{1\le u,v\le m\\u\ne x,\ v\ne y}}F(i-1,u,v).$$旧状态已经保证 ,因此不必在求和中重复检查。记旧层总和为 、第 行之和为 、第 列之和为 ,容斥得到
这把一次转移降至常数时间,但每列仍有 个状态,总成本为 。
问题出在完全空白的列。如果某一列已有至少一个士兵,就选一个已知兵种作为这列的固定端:优先选上格,否则选下格。固定端的位置和兵种已知,只需记录另一格的兵种,共 种可能。
把这些列称为关键列,按顺序记位置为 ,位置从 开始。设 为填好直到第 个关键列、该列固定端取已知兵种、另一格取 的方案数。关键列之间的内部列全为空,我们先解决如何一次跨过它们。
用七类关系描述空白区间
固定左端列的上、下兵种为不同的 。两个端点列的下标相差 ,所以中间有 列; 表示同一列。
对于一组指定的右端兵种 ,区间方案数只取决于它们与 的相等关系。原因是:除 外,其他兵种可整体重命名,重命名给出合法方案的一一对应。
设 为相应类别中一个指定右端有序对的方案数。它不是这一类所有右端对的总和。七种类别如下, 表示不等于 的兵种,最后一类还要求 。
右端上下兵种 同类有序对的数量 同列相同兵种非法,故没有其他类别。初始条件为 ,其余 。
预处理下一距离时,仍使用上一节的容斥。为简洁,下面的 表示 。旧层总和是
对某个指定兵种求行和、列和,有
$$\begin{aligned} R_A&=h_0+(m-2)h_2,& C_A&=h_1+(m-2)h_3,\\ R_B&=h_1+(m-2)h_4,& C_B&=h_0+(m-2)h_5,\\ R_O&=h_3+h_5+(m-3)h_6,& C_O&=h_2+h_4+(m-3)h_6. \end{aligned}$$例如,上格已经是指定的 ,下格可以为 、,或者除 外的 种兵种。这里再次体现了“一个指定有序对”与“整类总和”的区别。
若类别 的右端对为 ,则
对每个 计算七个值,预处理为线性时间。于是,对于具体端点 、,比较兵种相等关系即可在常数时间取得区间权重 ;任一端点两兵种相同,权重为零。
关键列之间的求和为什么能变成一次函数
考虑相邻两个关键列,旧固定兵种为 ,新固定兵种为 。旧另一格的兵种记为 ,新另一格记为 。设 为按两列实际上下位置排列端点后得到的 。若固定端从上格变成下格,要交换对应端点对的顺序,不能只比较两个固定兵种。
令 ,则尚未筛选新列另一格的预置限制时,完整转移为
旧列已经保证 ,新列必须令 。
先考虑普通兵种 。在所有有贡献的 中,只须区分:、、以及其余兵种。对最后一类,端点相等关系完全一致,所以权重相同。
选一个不属于 的代表 。题目保证 ,这样的代表总能找到。令
则
$$\begin{aligned} \widetilde f(j,z) &=V W(q,z)+f(z)W(z,z)+(S-V-f(z))H\\ &=\alpha f(z)+\beta,\\ \alpha&=W(z,z)-H,\\ \beta&=S H+V\bigl(W(q,z)-H\bigr). \end{aligned}$$对于所有普通 ,三个权重所代表的相等关系都相同,因此 也相同。只取一个普通代表就能计算这两个系数。若 ,则 ,相关退化项自然消失。
剩下的新兵种 需要单独计算。当 时,旧 只分为 和其余有效兵种:
当 时该值就是零。因此每次关键列转移只需:对所有兵种统一做 ,再修正兵种 和 两处。
例如 ,两列的上格分别固定为 ,下格未定。第一列的状态为 ,其余为 ,故 。对于新下格 ,旧下格只须排除 ,得到 ;新下格取 时,旧下格原本就不能取 ,故有 种;取 时非法。答案为 。这展示了统一更新和两个例外分别承担的作用。
若新列上下两格都已固定,仍按上述方式转移,随后只保留另一格的指定兵种,其他状态全部清零。若上下固定兵种相同,被保留的值本来就是零。
延迟执行全体兵种的更新
若每次显式更新 个状态,可以得到 的算法。最大范围下 都可达 ,需要进一步避免逐兵种更新。
把当前每个兵种的值表示为
同时保存总和 。一开始可令所有 ,通过 指定统一初值。
当全体状态执行 且 时,只修改
$$M\leftarrow\alpha M,\qquad B\leftarrow\alpha B+\beta,\qquad S\leftarrow\alpha S+m\beta.$$由于 是质数,非零 有逆元。为把一个兵种的值设为 ,先查出旧值并修正总和,再写入
同时维护 ;一次非零全体乘法后,它乘上 。快速幂计算该逆元。
不能假设 总非零。例如相邻两列的固定端分处不同行时,某些转移的普通状态会全部变成同一个值。当 ,所有状态应为 ,此时令 ,并把所有旧的 视为零。
为了不遍历数组清零,为每个兵种保存写入时的版本号,并维护当前版本。全体赋值时增加版本号;查询到旧版本时按 处理。后续单点写入更新其版本。这也直接支持双固定列的“全清零后保留一个值”。
每次转移先读出旧总和、,算好两个普通系数及例外值,再执行更新,避免把新层值误作旧层值。
两端空白列与最终答案
设某一列已指定不同的上下兵种。与它相邻的一列全部空白时,上下兵种需要分别避开它们,并且彼此不同。先分别选择得到 种,其中两格相同且不等于原来两种兵种的情况有 种,所以扩展数为
这个数与指定的具体兵种无关。因此,最左关键列在位置 时,初始状态为
$$f(1,c)= \begin{cases} D^{\ell_1},&c\ne p_1\text{ 且符合该列另一格的预置限制},\\ 0,&\text{其他情况}. \end{cases}$$依次转移所有关键列后,右边剩余 列也逐列贡献 ,答案为
$$\left(\sum_{c=1}^{m}f(k,c)\right)D^{n-1-\ell_k}\pmod P.$$若完全没有关键列,第一列有 种选择,其余每列贡献 ,答案直接为 。 时上述公式也成立。预先保存 即可。
逐列容斥保证了区间权重的正确性,七类合并来自兵种重命名的一一对应,关键列递推枚举了全部端点兵种且没有重复。仿射表示仅改变状态的维护方式,不改变该递推;再结合两端的独立扩展,覆盖了完整网格的所有合法方案。
复杂度
时间复杂度为 ,其中 是关键列数量:空段七类和幂表预处理为 ,每次关键列转移只有常数次查询、赋值及至多一次模逆元快速幂。
空间复杂度为 。保留全部距离的七类状态和输入;关键列完整逻辑表有 项,最大约 GB,超过空间限制,因此只用仿射表示维护当前层,并在更新前保存所需的旧值。
AC 代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; int norm(i64 x) { return (x % mod + mod) % mod; } int power(int a, int b) { int res = 1; while (b) { if (b & 1) res = 1LL * res * a % mod; a = 1LL * a * a % mod; b >>= 1; } return res; } // value(x) = mul * raw[x] + add; an old timestamp means raw[x] = 0. struct Affine { int n, epoch = 0, mul = 1, inv = 1, add = 0, sum = 0; vector<int> raw, stamp; Affine(int n) : n(n), raw(n + 1), stamp(n + 1) {} int query(int x) { return norm(1LL * mul * (stamp[x] == epoch ? raw[x] : 0) + add); } void set(int x, int value) { sum = norm(1LL * sum - query(x) + value); raw[x] = 1LL * norm(1LL * value - add) * inv % mod; stamp[x] = epoch; } void apply(int a, int b) { sum = (1LL * a * sum + 1LL * n * b) % mod; if (a == 0) { epoch++; mul = inv = 1; add = b; } else { mul = 1LL * mul * a % mod; inv = 1LL * inv * power(a, mod - 2) % mod; add = (1LL * a * add + b) % mod; } } }; void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<int> pos; for (int i = 0; i < n; i++) { if (a[i] || b[i]) pos.push_back(i); } const int ways = norm(1LL * m * m - 3LL * m + 3); vector<int> pw(n + 1, 1); for (int i = 1; i <= n; i++) pw[i] = 1LL * pw[i - 1] * ways % mod; if (pos.empty()) { cout << 1LL * m * (m - 1) % mod * pw[n - 1] % mod << '\n'; return; } // Relative to fixed left colors A,B: AB,BA,AO,OA,BO,OB,OO'. const vector<pair<int, int>> type = {{0, 1}, {1, 0}, {0, 2}, {2, 0}, {1, 2}, {2, 1}, {2, 2}}; vector<array<int, 7>> dp_gap(n); dp_gap[0][0] = 1; for (int d = 1; d < n; d++) { const auto& pre = dp_gap[d - 1]; i64 sum = norm(pre[0] + 1LL * pre[1] + 1LL * (m - 2) * (0LL + pre[2] + pre[3] + pre[4] + pre[5]) + 1LL * (m - 2) * (m - 3) % mod * pre[6]); vector<int> row = {norm(pre[0] + 1LL * (m - 2) * pre[2]), norm(pre[1] + 1LL * (m - 2) * pre[4]), norm(pre[3] + 1LL * pre[5] + 1LL * (m - 3) * pre[6])}; vector<int> col = {norm(pre[1] + 1LL * (m - 2) * pre[3]), norm(pre[0] + 1LL * (m - 2) * pre[5]), norm(pre[2] + 1LL * pre[4] + 1LL * (m - 3) * pre[6])}; for (int t = 0; t < 7; t++) { auto [x, y] = type[t]; dp_gap[d][t] = norm(sum - row[x] - col[y] + pre[t]); } } auto kernel = [&](int d, int x, int y, int u, int v) { if (x == y || u == v) return 0; int t; if (u == x) t = v == y ? 0 : 2; else if (u == y) t = v == x ? 1 : 4; else if (v == x) t = 3; else if (v == y) t = 5; else t = 6; return dp_gap[d][t]; }; Affine dp(m); int last = pos[0], side = a[last] ? 0 : 1; int p = side == 0 ? a[last] : b[last]; dp.apply(0, pw[last]); dp.set(p, 0); if (a[last] && b[last]) { int value = dp.query(b[last]); dp.apply(0, 0); dp.set(b[last], value); } for (int j = 1; j < (int)pos.size(); j++) { int i = pos[j], next_side = a[i] ? 0 : 1; int q = next_side == 0 ? a[i] : b[i]; int d = i - last; auto weight = [&](int y, int z) { int x = p, u = q; if (side) swap(x, y); if (next_side) swap(u, z); return kernel(d, x, y, u, z); }; int z = 1; while (z == p || z == q) z++; int w = 1; while (w == p || w == q || w == z) w++; int sum = dp.sum, value = dp.query(q); int other = weight(w, z); int mul = norm(1LL * weight(z, z) - other); int add = norm(1LL * sum * other + 1LL * value * (weight(q, z) - other)); int special = p == q ? 0 : norm(1LL * value * weight(q, p) + 1LL * norm(1LL * sum - value) * weight(w, p)); dp.apply(mul, add); dp.set(p, special); dp.set(q, 0); if (a[i] && b[i]) { int keep = dp.query(b[i]); dp.apply(0, 0); dp.set(b[i], keep); } last = i; side = next_side; p = q; } cout << 1LL * dp.sum * pw[n - 1 - last] % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
子任务 3:同一行预置与目标兵种截面(通过子任务 3,共 25 分)
利用第二行没有预置士兵的条件
子任务 3 满足 ,所有预置士兵都在第一行,且第一列、最后一列都有预置士兵。把第一行非空的列称为关键列,位置依次为 。
在关键列处,上格兵种已经确定,只需记录下格兵种。设 为填好前 列、满足这些列全部预置条件、且第 列下格兵种恰好为 的方案数,。若这列上格兵种为 ,则 。
难点是两列之间可能有完全空白的列:经过一列空白后,上格不再固定,不能继续只保存下格的分布。但我们提前知道下一个关键列要求上格等于哪个兵种,因此不必保存全部 个有序对,只需维护行和、列和,以及那个指定上格兵种对应的一行状态。
从完整列状态推出封闭的统计量
考虑相邻关键列 ,令 ,右端上格固定兵种为 。在本段内,设 为填好前 列、当前列上下兵种恰好为 的方案数,。它保留左端及以前的预置条件,暂不施加右端的预置条件;同列相同兵种始终非法,即 。
定义
$$S(t)=\sum_{x=1}^{m}\sum_{y=1}^{m}F(t,x,y),\qquad R(t,x)=\sum_{y=1}^{m}F(t,x,y),\qquad C(t,y)=\sum_{x=1}^{m}F(t,x,y).$$这里的行、列是兵种状态矩阵的行、列,不是网格的物理行、列。再定义目标兵种截面
只统计当前上格恰好为 的方案,并不意味着中间每列都被强制填成 。中间的全部方案仍由 统计,不能把 当作 。
当下一列暂时不设预置限制时,新上格为 、下格为 ,需要分别排除旧上格等于 、旧下格等于 的方案。容斥得到,对 ,
对所有 求和,利用 ,得到
$$\begin{aligned} R(t+1,x) &=(m-1)\bigl(S(t)-R(t,x)\bigr) -\bigl(S(t)-C(t,x)\bigr)+R(t,x)\\ &=(m-2)\bigl(S(t)-R(t,x)\bigr)+C(t,x). \end{aligned}$$上下对称,另一项为
对一个已经指定了不同上下兵种的旧列,下一列有 种分别避开水平冲突的选择,其中上下相同的非法选择有 种。记
则 。目标截面直接使用完整转移:
$$H(t+1,c)= \begin{cases} S(t)-R(t,q)-C(t,c)+H(t,c),&c\ne q,\\ 0,&c=q. \end{cases}$$这些方程只依赖 的上一层,因而三张长度为 的表与一个总和已经足够。所有运算均对 取模,减法须归一化。
同一行预置时如何开始和结束一段
令左端上格兵种为 ,,。左端状态矩阵只有第 行可能非零,因此
$$\begin{aligned} S(0)&=s,\\ R(0,c)&=[c=p]\,s,\\ C(0,c)&=f(c),\\ H(0,c)&=[p=q]\,f(c). \end{aligned}$$表示命题 成立时为 ,否则为 。当 时,目标截面初始全零,但并不代表没有合法方案:经过空白列转移,其他上格兵种的状态仍可进入这个截面。
从 递推到 后,右端要求上格等于 ,所以直接令
下一段的总和必须重新取 ,不能沿用尚未施加右端限制的 。实现中最后一步只需要计算 ,不必再计算随后会丢弃的 。
例如 ,相邻两列上格分别固定为 。第一列有 ,其余四项为 ,故 。目标截面初始为零,。新下格取 时有 种;取 时各有 种;取 非法,合计 种。
初始化、收尾与适用范围
本子任务第一列已经有预置上格 ,所以 。最后一列也是关键列,最终答案为 。 时不发生段间转移,答案为 。
代码顺带处理同一结构下首尾为空的情形。固定一个合法端点列,向外延长一列空白总有 种方法,所以初值一般写为
最终答案乘 。若没有任何预置士兵,答案为 。预处理 的幂即可。这没有解除“第二行没有预置士兵”的前提,不能用于一般的上下两行混合预置。
完整转移枚举了所有合法的相邻列,行列和方程是对该转移的精确求和,截面方程也未舍弃会进入目标状态的前驱;在右端保留截面恰好施加了该列的预置条件。因此逐段递推与原来的完整状态 DP 等价。
复杂度
时间复杂度为 。各段距离之和不超过 ,每步扫描 个兵种,每个关键列重新初始化统计量也只需 ;幂表另需 。
空间复杂度为 。若保留所有处理位置的 ,最坏需要约 个整数,在 时约为 GB,超过 MB,因此只保存旧、新两层。所有新层值都从旧层计算,整层完成后交换;关键列上的 正是筛选后的截面,不另存重复的历史表。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; int norm(i64 x) { return (x % mod + mod) % mod; } void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<int> pos; for (int i = 0; i < n; i++) { if (a[i]) pos.push_back(i); } int ways = norm(1LL * m * m - 3LL * m + 3); vector<int> pw(n + 1, 1); for (int i = 1; i <= n; i++) pw[i] = 1LL * pw[i - 1] * ways % mod; if (pos.empty()) { cout << 1LL * m * (m - 1) % mod * pw[n - 1] % mod << '\n'; return; } int last = pos[0], p = a[last]; vector<int> dp(m + 1, pw[last]), ndp(m + 1); vector<int> row(m + 1), col(m + 1); vector<int> next_row(m + 1), next_col(m + 1); dp[0] = dp[p] = 0; for (int j = 1; j < (int)pos.size(); j++) { int r = pos[j], q = a[r]; int sum = accumulate(dp.begin(), dp.end(), 0LL) % mod; fill(row.begin(), row.end(), 0); row[p] = sum; col = dp; if (p != q) fill(dp.begin(), dp.end(), 0); for (int i = last + 1; i <= r; i++) { i64 base = 1LL * sum - row[q]; for (int x = 1; x <= m; x++) { ndp[x] = x == q ? 0 : norm(base - col[x] + dp[x]); } if (i < r) { for (int x = 1; x <= m; x++) { next_row[x] = norm(1LL * (m - 2) * (sum - row[x]) + col[x]); next_col[x] = norm(1LL * (m - 2) * (sum - col[x]) + row[x]); } row.swap(next_row); col.swap(next_col); sum = 1LL * sum * ways % mod; } dp.swap(ndp); } last = r; p = q; } int ans = accumulate(dp.begin(), dp.end(), 0LL) % mod; cout << 1LL * ans * pw[n - 1 - last] % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
用行列和优化列状态转移(通过子任务 1–2,共 20 分)
列状态与不合法前驱
设 为前 列已填好,第 列上下兵种分别为 的方案数。 均在 中;两者相同,或与该列预置不符时,状态恒为零。第一列每个合法状态为 。
对于合法的新列 ,上一列 只需满足 ,因此完整递推为
$$F(i,x,y)=\sum_{\substack{1\le u,v\le m\\u\ne x,\ v\ne y}}F(i-1,u,v).$$当 时,状态数量仍可接受,但不能对每个状态重新扫描旧状态。
用容斥同时排除两条水平边冲突
对上一层预先求出
$$S(i-1)=\sum_{u=1}^{m}\sum_{v=1}^{m}F(i-1,u,v),\quad R(i-1,x)=\sum_{v=1}^{m}F(i-1,x,v),\quad C(i-1,y)=\sum_{u=1}^{m}F(i-1,u,y).$$从所有旧方案中,扣掉旧上格为 的方案、旧下格为 的方案。旧列恰为 的方案被扣了两次,需要加回一次:
$$F(i,x,y)=S(i-1)-R(i-1,x)-C(i-1,y)+F(i-1,x,y)\pmod P.$$例如新列为 ,两类冲突分别是旧列形如 和 ,交集恰为 。这里加回旧状态不是允许冲突,而是把被扣两次的交集恢复到“恰好扣一次”。
每次先从完整旧层统计总和、行和、列和,再枚举合法的新状态,按该式赋值;不符合新列预置的状态保持零。所有等式都在模 下计算,减法要归一化。
最后输出
状态保留全部可能的最后一列,递推恰好检查所有新出现的边;归纳可知它计算了所有合法方案,包括答案为零的情况。
时间复杂度为 。每层两次扫描兵种对,分别统计旧层和填写新层。空间复杂度为 ,保留完整按列状态;在 的保证范围内,状态与容器合计仍在题目 512 MB 限制内。该方法覆盖子任务 1、2。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<vector<vector<int>>> dp(n, vector<vector<int>>(m + 1, vector<int>(m + 1))); for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) { if (x != y && (!a[0] || a[0] == x) && (!b[0] || b[0] == y)) dp[0][x][y] = 1; } } for (int i = 1; i < n; i++) { vector<int> row(m + 1), col(m + 1); i64 sum = 0; for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) { int value = dp[i - 1][x][y]; row[x] = (row[x] + value) % mod; col[y] = (col[y] + value) % mod; sum += value; } } sum %= mod; for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) { if (x == y || (a[i] && a[i] != x) || (b[i] && b[i] != y)) continue; i64 value = sum - row[x] - col[y] + dp[i - 1][x][y]; dp[i][x][y] = (value % mod + mod) % mod; } } } i64 ans = 0; for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) ans += dp[n - 1][x][y]; } cout << ans % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
按列枚举并合并一维前驱(通过子任务 1,共 10 分)
固定一列的两个兵种
当 时,可以直接记录最后一列上下格子的兵种。设 为已经填好前 列,且第 列上下兵种分别为 的方案数,。
若 ,或者 不符合该列已驻扎士兵的兵种,状态为零。第一列每个合法状态的值为 。
往右增加一列,只多出上下两条水平边和新列的一条竖边。竖边已由新状态 保证。旧列上下为 时,剩余条件恰好是 ,故
$$F(i,x,y)=\sum_{\substack{1\le u,v\le m\\u\ne x,\ v\ne y}}F(i-1,u,v)$$对所有合法新状态成立。每个完整方案都有唯一的上一列状态,因此上述求和没有遗漏或重复。
先合并一个枚举维度
直接枚举 太慢。先求旧层按上格兵种分类的总数
固定旧上格为 后,只须从该行总和扣掉旧下格恰为 的状态,因此
$$F(i,x,y)=\sum_{\substack{1\le u\le m\\u\ne x}} \bigl(R(i-1,u)-F(i-1,u,y)\bigr)\pmod P.$$例如新列为 ,旧上格不能为 ;对其他每个旧上格,从整行中去掉旧下格为 的方案即可。即使原始预置已经冲突,也只会导致相应状态为零,无需更改题意或单独假设存在合法方案。
按 推进,每层先完整计算旧层行和,再填新层。最终答案为
$$\sum_{x=1}^{m}\sum_{y=1}^{m}F(n,x,y)\pmod P,\qquad P=10^9+9.$$时间复杂度为 :每层求行和用 ,每个新状态再枚举 个旧上格。空间复杂度为 ,保存完整的按列状态。该方法适用于子任务 1;扩大 后三重循环迅速成为瓶颈。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; constexpr int mod = 1000000009; void solve() { int n, m; cin >> n >> m; vector<int> a(n), b(n); for (int& x : a) cin >> x; for (int& x : b) cin >> x; vector<vector<vector<int>>> dp(n, vector<vector<int>>(m + 1, vector<int>(m + 1))); for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) { if (x != y && (!a[0] || a[0] == x) && (!b[0] || b[0] == y)) dp[0][x][y] = 1; } } for (int i = 1; i < n; i++) { vector<int> row(m + 1); for (int x = 1; x <= m; x++) { row[x] = accumulate(dp[i - 1][x].begin(), dp[i - 1][x].end(), 0LL) % mod; } for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) { if (x == y || (a[i] && a[i] != x) || (b[i] && b[i] != y)) continue; i64 sum = 0; for (int u = 1; u <= m; u++) { if (u != x) sum += row[u] - dp[i - 1][u][y]; } dp[i][x][y] = (sum % mod + mod) % mod; } } } i64 ans = 0; for (int x = 1; x <= m; x++) { for (int y = 1; y <= m; y++) ans += dp[n - 1][x][y]; } cout << ans % mod << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); }
- 1
信息
- ID
- 2307
- 时间
- 3000ms
- 内存
- 512MiB
- 难度
- 9
- 标签
- (无)
- 递交数
- 11
- 已通过
- 1
- 上传者