Edu39
D. Timetable
转化成背包问题
dp预处理
很容易看出来是背包,但是怎么把区段内的时间转化成物品很吃力。
首先物品的体积肯定不能直接用题目给的时间点,而是用逃课的数量。
然后对物品的价值进行预处理
- 预处理出每天的日期
- 每次处理一天留下 \(k\) 个 \(1\) 的物品之前就先跑一遍找出贡献最小的方案
- 然后用这个方案来跑背包就行。
数据很小,不会超时
constexpr int N(500 + 10);
std::vector<int> sched[N];
int dp[N][N];
void solve() {
int n, m, k;
std::cin >> n >> m >> k;
std::string s;
for (int i = 1; i <= n; i++) {
std::cin >> s;
for (int j = 0; j < m; j++) {
if (s[j] == '1') {
sched[i].push_back(j);//预处理出每一天的1的日期
}
}
}
memset(dp, 0x3f, sizeof dp);
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
int siz(sz(sched[i]));
for (int cnt = 0; cnt <= siz; ++cnt) {
int res = m;//先求出当前选择cnt个1的能得到的最小贡献
if (cnt) {
for (int l = 0, r = cnt - 1; r < siz; ++l, ++r) {//暴力枚举一边所有区间
res = std::min(res, sched[i][r] - sched[i][l] + 1);
}
}
else {//如果是选择0个,那显然贡献就是0
res = 0;
}
int cntSkip(siz - cnt);
for (int l = 0; l + cntSkip <= k; ++l) {//已经转化成了背包问题
dp[i][l + siz - cnt] = std::min(dp[i][l + cntSkip], dp[i - 1][l] + res);
}
}
}
std::cout << *std::min_element(dp[n], dp[n] + k + 1) << '\n';
}
E. Largest Beautiful Number
贪心构造和bitmask的妙用
void solve()
{
#define tests
std::string s;
std::cin >> s;
int n(sz(s));
std::string res;
for (int i = 0; i < n - 2; i++) {
res += '9';
}
for (int lcp = n - 1; lcp >= 0; lcp--) {//从右到左检查,一旦遇到合适的位就输出
if (s[lcp] != '0') {
for (char c = s[lcp] - 1; c >= (lcp == 0 ? '1' : '0'); c--) {//枚举该位的所有可能数码
std::string cur = s.substr(0, lcp) + c;
int mask = 0;//用位来当数码,标记这一位对应的数码是否出现偶数次
for (int i = 0; i < sz(cur); i++) {
mask ^= (1 << (cur[i] - '0'));
}
std::string me;
for (int i = 9; i >= 0; i--) {
if ((mask >> i) & 1) {
me += (i + '0');//为落单的凑,从大到小凑,尽量接近给的数
}
}
while (sz(cur) + sz(me) < sz(s)) {//剩下的全部凑9
cur += '9';
}
cur += me;
if (sz(cur) == sz(s)) {
res = cur;
break;
}
}
if (sz(res) == sz(s)) {
break;
}
}
}
std::cout << res << '\n';
}
F. Fibonacci String Subsequences
思路
考虑每个 \(F(x)\) 中与 \(s\) 相同的子序列的贡献。设这个子序列为 \(F(x)_{p_1}, F(x)_{p_2}, F(x)_{p_3}, \ldots, F(x)_{p_n}\)。
我们想要它成为一个子序列的子串,那么 \(F(x)_{[p_1, p_n]}\) 中除了 \(p_1 \sim p_n\) 就不能有别的字符被选。而 \([1, p_1 - 1] \cup [p_n + 1, |F(x)|]\) 中的每个字符都可以选或不选,相当于每个字符产生 \(2\) 的贡献。
如果我们设 \(f_{i, j}\) 为 kmp 过程中考虑到 \(F(x)\) 的第 \(i\) 位,匹配到 \(s\) 的第 \(j\) 位的答案,那么相当于
\(f_{i + 1, 0} \gets 2 f_{i, 0}, f_{i + 1, n} \gets 2 f_{i, n}, \forall j \in [1, n - 1], f_{i + 1, j} \gets f_{i, j}, \forall j \in [1, n] \land F(x)_i = s_j, f_{i + 1, j} \gets f_{i, j - 1}\)。
答案即为 \(f_{|F(x)|, n}\)。
可以用矩阵刻画这个转移过程。设 \(F_i\) 为 \(F(i)\) 的转移矩阵,那么 \(F_i = F_{i - 1} F_{i - 2}\)。
在很多问题中,我们在处理递推公式或者动态规划的转移方程时,第 \(i\) 项可以由之前的 \(k\) 项的值推得,即有:
\(f_i = \sum^k_{j = 1} {a_j\times f_{i - j}}\)
构造一个这样的矩阵
\(A = \begin{pmatrix} 0& 0&\dots& 0& 0& a_k\\ 1& 0&\dots& 0& 0& a_{k - 1}\\ 0& 1&\dots& 0& 0& a_{k - 2}\\ \dots \\ 0& 0&\dots& 0& 1& a_{1} \end{pmatrix}\)
则有
\((f_{i - k}\dots f_{i - 2}, f_{i - 1})\times A = (f_{i - k + 1}\dots f_{i - 1}, f_i)\)
即矩阵 \(k\) 能把这一段 \(f\) 整体右移。
当 \(n\) 远大于 \(k\) 时,矩阵能起到显著的加速作用。
如何计算 \(f_n\):
\((f_{1}\dots f_{k - 1}, f_{k})\times A^{n - k} = (f_{n - k + 1}\dots f_{n - 1}, f_n)\)
即从 \(f_k\) 移动到 \(f_n\),移动 \(n - k\) 次,即乘以 \(n - k\) 次 \(A\).
以此类推,针对题目构造不同的矩阵
时间复杂度 \(O(n^3x)\)。
代码
constexpr int N(110);
int n, m;
std::string s;
struct mat {
Z a[N][N];
mat() {
memset(a, 0, sizeof a);
}
}dp[N];
mat operator * (const mat &a, const mat &b) {//A的列数等于B的行数才可以矩阵乘
mat res;
for (int k = 0; k <= n; ++k) {
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= n; ++j) {
res.a[i][j] += a.a[i][k] * b.a[k][j];
}
}
}
return res;
}
void solve() {
std::cin >> n >> m >> s;
dp[0].a[0][0] = dp[0].a[n][n] = dp[1].a[0][0] = dp[1].a[n][n] = 2;
for (int i = 1; i < n; i++) {
dp[0].a[i][i] = dp[1].a[i][i] = 1;
}
for (int i = 1; i <= n; i++) {
if (s[i - 1] == '0') {
dp[0].a[i - 1][i] = 1;
}
else {
dp[1].a[i - 1][i] = 1;
}
}
for (int i = 2; i <= m; i++) {
dp[i] = dp[i - 1] * dp[i - 2];
}
mat ans;
ans.a[0][0] = 1;
ans = ans * dp[m];
std::cout << ans.a[0][n] << '\n';
}