双周赛 56 单周赛 249 部分题解
知识点:图论,动态规划,字符串,状态压缩
规定时间内到达终点的最小花费
给定 \(n\) 个点,\(m\) 条边的双向图,每个点有点权 \(p_{i}\),边有边权,表示消耗的时间,现在要求计算出在指定时间 \(t\) 内,从点 \(0\) 到点 \(n - 1\) 的最小花费,花费值为路径上点的点权之和
题解
定义 \(dp[i][j]\) 表示消耗时间 \(i\),走到点 \(j\) 的最小花费
枚举前继节点 \(pre\),设 \(pre\) 到 \(j\) 的时间为 \(w\),那么状态转移方程为
时间复杂度 \(O(t\cdot (m + n))\)
class Solution {
public:
#define pb push_back
#define pii pair<int, int>
#define fi first
#define se second
#define INF 0x3f3f3f3f
const int N = 1e3 + 7;
int minCost(int m, vector<vector<int>>& e, vector<int>& p) {
int n = p.size();
vector<vector<int>> dp(n, vector<int>(m + 7, INF));
vector<pii> g[N];
for (int i = 0; i < e.size(); ++i) {
int x = e[i][0], y = e[i][1], z = e[i][2];
g[x].pb({y, z});
g[y].pb({x, z});
}
dp[0][0] = p[0];
for (int k = 0; k <= m; ++k) {
for (int i = 0; i < n; ++i) {
for (auto &j: g[i]) {
if (j.se + k <= m)
dp[j.fi][j.se + k] = min(dp[j.fi][j.se + k], dp[i][k] + p[j.fi]);
}
}
}
int ans = INF;
for (int i = 0; i <= m; ++i) if (dp[n - 1][i] != INF) cout << i << ' ' << dp[n - 1][i] << endl, ans = min(ans, dp[n - 1][i]);
return ans == INF ? -1 : ans;
}
};
无后效性
注意,如果定义 \(dp[i][j]\) 表示走到点 \(i\),消耗时间为 \(j\) 的的最小花费,并按照先点后时间的顺序转移,计算的答案会出现错误
例如 0 <-> 2 <-> 1 <-> 3
的路径,我们事实上应该按照 0 -> 2 -> 1 -> 3
的顺序转移,但我们实际上按照了 0 -> 1 -> 2 -> 3
的顺序转移,归根结底在于, 2
没有对 1
贡献
也就是说,出现了 \(pre > j\),在计算 \(j\) 的时候,\(pre\) 还没计算出来
回到无后效性的定义,当前状态不应该由后面的状态决定,因此上述的转移链不满足无后效性,但是如果第一维状态为时间,那么就满足了无后效性
事实上,我们只能在 \(DAG\) 上按照拓扑关系进行 \(dp\),这样可以保证计算到 \(j\) 时,\(pre\) 一定已经被计算出来,从而满足了无后效性
从表格的角度理解,当前行列只能由上一行或者当前行的前一列转移过来,不能由下一行或者当前行的后一列转移而来,当你的转移顺序不满足这个要求的时候,动态规划就会出错
长度为 \(3\) 的不同回文子序列
给定小写字母组成的字符串 \(s\),统计其中长度为 \(3\) 的不同的回文子序列个数
规定 \(3\leq s.length\leq 10^5\)
题解
对于每一个字符 \(i\),我们找到它出现的第一个位置 \(L\) 和最后一个位置 \(R\),然后统计 \([L + 1,\ R - 1]\) 之间有多少个不同的字符即可
我们用一个二维的桶 \(dict[i][j]\) 来维护 \([1,\ i]\) 中字符 \(j\) 出现的次数,然后便可以统计指定区间内不同字符的个数
时间复杂度 \(O(26\cdot n + 26^2)\)
class Solution {
public:
int countPalindromicSubsequence(string s) {
int n = s.length();
vector<int> L(26), R(26);
vector<vector<int>> dict(n + 1, vector<int>(26));
for (int i = 0; i < n; ++i)
if (!L[s[i] - 'a']) L[s[i] - 'a'] = i + 1; // 'a' + i 第一次出现的位置
for (int i = n - 1; i >= 0; --i)
if (!R[s[i] - 'a']) R[s[i] - 'a'] = i + 1; // 'a' + i 最后一次出现的位置
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < 26; ++j) {
dict[i][j] = dict[i - 1][j];
}
dict[i][s[i - 1] - 'a']++;
}
int ans = 0;
for (int i = 0; i < 26; ++i) {
int l = L[i], r = R[i], temp = 0;
if (r && l && r > l + 1) {
for (int j = 0; j < 26; ++j) {
temp += (dict[r - 1][j] - dict[l][j] > 0);
}
//cout << l << ' ' << r << ' ' << temp << endl;
ans += temp;
}
}
return ans;
}
};
用三种不同颜色为网格涂色
给定 \(m\times n\) 的网格,用 \(RGB\) 三种颜色染色,要求相邻网格颜色不重复,计算可能的染色方案数,答案对 \(10^9 + 7\) 取模
\(1\leq m\leq 5\)
\(1\leq n\leq 1000\)
题解
注意到 \(m\) 很小,可以考虑对列的染色情况进行状态压缩,一列一共有 \(3^m\) 种染色情况
考虑状态压缩 \(dp\),定义 \(dp[i][j]\) 表示第 \(i\) 列,染色情况为 \(j\) 的方案数,那么状态转移方程为
其中 \(k,\ j\) 均为 \(3\) 进制数,并且满足每一位都不相同,且自身相邻位不相同
时间复杂度 \(O(n\cdot 3^{2m})\),使用哈希表预处理合法的列状态可以优化到 \(O(n\cdot 3^{m})\)
class Solution {
public:
const int MOD = 1e9 + 7;
typedef long long LL;
bool check_row(int m, int x, int y) {
for (int i = 0; i < m; ++i) {
if (x % 3 == y % 3) return 0;
x /= 3, y /= 3;
}
return 1;
}
bool check_col(int m, int x) {
int y = x / 3;
for (int i = 1; i < m; ++i) {
if (x % 3 == y % 3) return 0;
x /= 3, y /= 3;
}
return 1;
}
int colorTheGrid(int m, int n) {
LL tot = 1;
for (int i = 1; i <= m; ++i) tot *= 3;
vector<vector<LL>> dp(n + 1, vector<LL>(tot + 1));
for (int i = 0; i < tot; ++i)
if (check_col(m, i)) dp[1][i] = 1 % MOD;
for (int i = 2; i <= n; ++i) {
for (int j = 0; j < tot; ++j) {
if (!check_col(m, j)) continue;
for (int k = 0; k < tot; ++k) {
if (check_col(m, k) && check_row(m, j, k)) {
dp[i][j] += dp[i - 1][k], dp[i][j] %= MOD;
}
}
}
}
LL ans = 0;
for (int i = 0; i < tot; ++i) ans += dp[n][i], ans %= MOD;
return ans;
}
};