1 条题解
-
0
自动搬运
来自洛谷,原作者为

CuteChat
**搬运于
2025-08-24 23:15:47,当前版本为作者最后更新于2025-02-08 14:19:30,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
题解
如果您并没有数位动态规划的基础,可以看看出题人写的
烂笔记。Subtask 1
是 的幂次减一,相当于行的限制不管用了,而列的限制就遵循列的限制进行计算即可,答案易得 。
Subtask 3
这里是一个数位动态规划模板,本质就是问你 中,有多少个数字,满足任意的 都满足第 位小于等于 。
这个只需要记录一个标记表示是否小于 即可,从高到低位做,转移是显然的。
Subtask 4
这一步大概可以理解为 个数位动态规划同时进行,所以直接先记录 个标记,实际写代码的时候可以考虑状态压缩,用一个二进制数表示列标记的状态。
那么假定我们进行到了第 行,就直接枚举当前这一行的数字是什么(而不是一个数位),直接检查是否满足这 个标记的限制,并且直接转移即可。
时间复杂度是 ,感觉比正解复杂,不推荐写。
Subtask 5
数位动态规划的本质就是用来优化枚举的数字的,既然内层又套了一层枚举,就继续用数位动态规划优化。
具体来说,流程是从上到下、从左往右的顺序填写每一个格子,结合轮廓线的思想,只维护当前这个格子所对应轮廓线上的标记状压值即可。
具体来说,整个算法的运行状况大约如图下所示(蓝色和绿色的就是轮廓线):

比如目前的格子,如果填写 ,那么当前这一行(绿色)的标记会改变为 。而列(蓝色)的标记,如果仍然填写 ,那么标记不变。
转移后,第二列的蓝色轮廓线将会下降一格,备用于下一列的处理,绿色轮廓线将会右移一格,与正常的动态规划无异。
这一档很宽松,能保证不管常数有多大都能得到这一部分分。
Subtask 6, 7, 8
仔细分析 Subtask 5 的复杂度,发现是 的。似乎能能得到 分,但是实际上内部仍然是由一个枚举 的数位的过程。带上两倍常数运算量大约为 ,几乎过不了。
不过内部枚举 的数位其实只有两种本质不同的数位,一种是会让至少一种标记不会更新,一种是都不会让标记更新的。
第一种显然只会有一个数,也就是目前标记状态的情况下可以填写的最大数位,转移不需要任何代价。
第二种显然就是所有小于这个最大数位的数位,这些数位的转移都是一样的,因为不管怎样比较都是小于,直接加上新转移的状态值与方案数的乘积即可。
这样就省去了 倍的常数,由于空间不足以开 的数组,所以使用滚动数组优化。
代码(正常写法)
#include <bits/stdc++.h> using namespace std; const int N = 18; int n, m, dp[2][N][1 << N][2], r[N][N], c[N][N]; inline int stat(int i, int j, int lim, int llim) { // 获取正确的状态值,书写代码方便。 if (i == n - 1 && j == m) return 1; if (j == m) return dp[(i + 1) & 1][0][lim][0]; return dp[i & 1][j][lim][llim]; } signed main() { ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i = 0; i < n; ++i) { long long x; cin >> x; for (int j = m - 1; j >= 0; --j) { r[i][j] = x % 10; x /= 10; } } for (int i = 0; i < m; ++i) { long long x; cin >> x; for (int j = n - 1; j >= 0; --j) { c[i][j] = x % 10; x /= 10; } } for (int i = n - 1; i >= 0; --i) { for (int j = m - 1; j >= 0; --j) { for (int lim = 0; lim < (1 << m); ++lim) { for (int llim = 0; llim <= 1; ++llim) { long long ans = 0; int d1 = (llim ? 9 : r[i][j]); int d2 = ((1 << j) & lim) ? 9 : c[j][i]; int mind = min(d1, d2); // 计算出最大可以填的数位 ans += stat(i, j + 1, (lim | (mind != c[j][i] ? (1 << j) : 0)), llim | (mind != r[i][j])); if (mind) ans += 1ll * mind * stat(i, j + 1, lim | (1 << j), 1); // 剩下的就是固定的转移,只要乘方案数即可 dp[i & 1][j][lim][llim] = ans % 998244353; } } } } cout << dp[0][0][0][0] << "\n"; return 0; }挑战最优解(标程写法)
- 直接定义 的下一个格子是 ,于是又可以滚动掉无用的一维,这样不仅可以省下一个长度 的状态,还减少了时间常数。
- 可以用定期取模。通过估算发现 很接近了 位整数的范围,所以只需要在转移 次后统一取模即可。
- 循环的最后一层是枚举的是当前行的标记,这个循环直接展开即可。
- 在第三步基础上去除一些冗余代码。以及其他的卡常。
这样写跑得飞起,轻轻松松卡近 ,虽然第二条卡常很抽象,但即使不用也能跑到大约 。
#include <iostream> #include <algorithm> #define int long long using namespace std; const int N = 18, p = 998244353; int n, m, dp[2][1 << N][2]; int r[N*N], c[N*N]; signed main() { cin >> n >> m; for (int i = 0; i < n; ++i) { long long x; cin >> x; for (int j = m - 1; j >= 0; --j) { r[i * m + j] = x % 10; x /= 10; } } for (int j = 0; j < m; ++j) { long long x; cin >> x; for (int i = n - 1; i >= 0; --i) { c[i * m + j] = x % 10; x /= 10; } } for (int lim = 0; lim < (1 << m); ++lim) { for (int llim = 0; llim <= 1; ++llim) { dp[(n * m) & 1][lim][llim] = 1; } } for (int id = n * m - 1; id >= 0; --id) { // 优化 1,需要注意判断列边界。 for (int lim = 0; lim < (1 << m) ; ++lim) { // 循环展开,优化 3 int mind = std::min((int)r[id], ((lim >> (id % m)) & 1) ? 9 : c[id]); dp[id & 1][lim][0] = (dp[(id + 1) & 1][lim | (mind != c[id] ? (1 << (id % m)) : 0)][(id % m == m - 1 ? 0 : (mind != r[id]))] + 1ll * mind * dp[(id + 1) & 1][lim | (1 << (id % m))][(id % m == m - 1 ? 0 : 1)]); mind = ((lim >> (id % m)) & 1) ? 9 : c[id]; dp[id & 1][lim][1] = (dp[(id + 1) & 1][lim | (mind != c[id] ? (1 << (id % m)) : 0)][id % m != m - 1] + 1ll * mind * dp[(id + 1) & 1][lim | (1 << (id % m))][(id % m == m - 1 ? 0 : 1)]); } if (id % 9 == 0) { // 优化 2 for (int lim = 0; lim < (1 << m); ++lim) { dp[id & 1][lim][0] %= p; dp[id & 1][lim][1] %= p; } } } cout << dp[0][0][0] % p; // 记得最后取模 return 0; }闲话
如果您有什么特别厉害的正确做法比标程还要快很多,欢迎与出题人交流。
另外由于新的线路开通,为了修建 $\color{#f2a900}\dfrac{0}{6}\color{black}/\color{e4002b}\dfrac{1}{14}\color{black}/\color{862041}\dfrac{9}{4}\color{black}/\color{#665ec4}\dfrac{27}{9}$ 站的四线换乘我不知道需要多少世纪。
- 1
信息
- ID
- 10349
- 时间
- 1400ms
- 内存
- 512MiB
- 难度
- 5
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者