知识点:图论,动态规划,字符串,状态压缩
规定时间内到达终点的最小花费
给定 (n) 个点,(m) 条边的双向图,每个点有点权 (p_{i}),边有边权,表示消耗的时间,现在要求计算出在指定时间 (t) 内,从点 (0) 到点 (n - 1) 的最小花费,花费值为路径上点的点权之和
题解
定义 (dp[i][j]) 表示消耗时间 (i),走到点 (j) 的最小花费
枚举前继节点 (pre),设 (pre) 到 (j) 的时间为 (w),那么状态转移方程为
时间复杂度 (O(tcdot (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) 的不同的回文子序列个数
规定 (3leq s.lengthleq 10^5)
题解
对于每一个字符 (i),我们找到它出现的第一个位置 (L) 和最后一个位置 (R),然后统计 ([L + 1, R - 1]) 之间有多少个不同的字符即可
我们用一个二维的桶 (dict[i][j]) 来维护 ([1, i]) 中字符 (j) 出现的次数,然后便可以统计指定区间内不同字符的个数
时间复杂度 (O(26cdot 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 imes n) 的网格,用 (RGB) 三种颜色染色,要求相邻网格颜色不重复,计算可能的染色方案数,答案对 (10^9 + 7) 取模
(1leq mleq 5)
(1leq nleq 1000)
题解
注意到 (m) 很小,可以考虑对列的染色情况进行状态压缩,一列一共有 (3^m) 种染色情况
考虑状态压缩 (dp),定义 (dp[i][j]) 表示第 (i) 列,染色情况为 (j) 的方案数,那么状态转移方程为
其中 (k, j) 均为 (3) 进制数,并且满足每一位都不相同,且自身相邻位不相同
时间复杂度 (O(ncdot 3^{2m})),使用哈希表预处理合法的列状态可以优化到 (O(ncdot 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;
}
};