NOI2020 Day1
美食节
美食节比较麻烦,先不考虑它,即考虑 \(k = 0\) 的情况。
设 \(f_{i, u}\) 表示第 \(i\) 天在 \(u\) 的最大愉悦值。
转移:
初值 \(f_{0, 1} = c_1\),答案 \(f_{T, 1}\)。
时间复杂度 \(\mathcal O(Tm)\)。
看到这个巨大的 \(T\),结合 DP,容易联想到矩阵乘法。但是很不好的一点是大多数时候我们并不会从 \(f_i\) 转移到 \(f_{i + 1}\),而是 \(f_{i + w}\),这是矩阵乘法不好做的。
注意到 \(w\) 奇小,所以考虑重新设计状态,拆点,设 \(f_{i, j, u}\) 表示现在是第 \(i\) 天,还有 \(j\) 天才能够到达 \(u\)。
转移:
初值 \(f_{0, 0, 1} = c_1\),答案 \(f_{T, 0, 1}\)。
由转移可以看出 \(j \le 4\),所以一拆五。
时间复杂度 \(\mathcal O(n^3 \log T)\),但是因为拆点带了个 \(125\) 倍常数。
这就足够拿满 \(k = 0\) 的所有点了,继续考虑 \(k > 0\) 的情况:
其实 \(k\) 也不大,所以我们完全可以把时间分为至多 \(k + 1\) 段,每段分别转移,时间复杂度 \(\mathcal O(kn^3\log T)\),但是常数会变小不少,因为 \(\log T\) 是完全跑不满的。
考虑优化矩阵快速幂,每次求转移矩阵的 \(2\) 的幂的时间复杂度时 \(\mathcal O(n^3 \log T)\),把向量和转移矩阵乘起来求答案的时间复杂度是 \(\mathcal O(n^2 \log T)\),把前一部分单拿出来预处理,快速幂时直接乘就好。这样时间复杂度达到 \(\mathcal O((n^3 + kn^2) \log T)\),过过过。
代码
#include <bits/stdc++.h>
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ld = long double;
template<typename tp> inline void chkmax(tp &x, tp y) {x = max(x, y);}
template<typename tp> inline void chkmin(tp &x, tp y) {x = min(x, y);}
constexpr int N = 60, logT = 30, K = 210;
int n, m, T, k, c[N];
struct Matrix {
ll a[5 * N][5 * N];
Matrix() {memset(a, 0xaf, sizeof(a));}
} trans[logT];
Matrix mul(const Matrix &A, const Matrix &B, int n, int r, int m) {
Matrix res;
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) {
for (int k = 1; k <= r; k++) if (A.a[i][k] >= 0 && B.a[k][j] >= 0) {
chkmax(res.a[i][j], A.a[i][k] + B.a[k][j]);
}
}
return res;
}
struct Festival {
int t, x, y;
bool operator<(const Festival &rhs) const {return t < rhs.t;}
} f[K];
int main() {
ios_base::sync_with_stdio(0); cin.tie(nullptr), cout.tie(nullptr);
cin >> n >> m >> T >> k; int n5 = 5 * n;
for (int i = 1; i <= n; i++) {
cin >> c[i];
for (int j = 4; j; j--) trans[0].a[i + n * j][i + n * (j - 1)] = 0;
}
for (int u, v, w; m--;) {
cin >> u >> v >> w;
trans[0].a[u][v + (w - 1) * n] = c[v];
}
int bits = 0;
for (bits = 1; (1 << bits) <= T; bits++) trans[bits] = mul(trans[bits - 1], trans[bits - 1], n5, n5, n5);
for (int i = 1; i <= k; i++) cin >> f[i].t >> f[i].x >> f[i].y;
sort(f + 1, f + k + 1);
Matrix ans; ans.a[1][1] = c[1];
for (int i = 1; i <= k; i++) {
int t = f[i].t - f[i - 1].t;
for (int e = bits; e >= 0; e--) if ((t >> e) & 1) ans = mul(ans, trans[e], 1, n5, n5);
if (ans.a[1][f[i].x] >= 0) ans.a[1][f[i].x] += f[i].y;
}
if (f[k].t < T) {
int t = T - f[k].t;
for (int e = bits; e >= 0; e--) if ((t >> e) & 1) ans = mul(ans, trans[e], 1, n5, n5);
}
cout << max(ans.a[1][1], -1ll);
return 0;
}
命运
题意:给定一棵 \(n\) 个点的树和树上的 \(m\) 条链 \(u_i \to v_i\)(可能有重复),保证 \(u_i\) 是 \(v_i\) 的祖先。现在给树上的每条边赋一个 \(0/1\) 中的权值,满足每条链上至少有一条边的权值为 \(1\) 的方案数。
记以 \(u\) 为根的子树为 \(\mathcal T_u\)。
树形 DP,只考虑下端点在 \(\mathcal T_u\) 中的链。
链有两种类型:
-
下端点在 \(\mathcal T_u\) 中,上端点在 \(1 \to fa_u\) 上。
-
两个端点都在 \(\mathcal T_u\) 中。
对于第一种类型的链,一个性质是若存在两条链 \(x_1 \to y_1\),\(x_2 \to y_2\),且满足 \(x_1, x_2 \in 1 \to fa_u\),\(y_1, y_2 \in \mathcal T_u\),\(dep_{x_1} > dep_{x_2}\),则如若 \(x_1 \to y_1\) 合法,\(x_2 \to y_2\) 一定合法。
设 \(f_{u, d}\) 表示至少下端点在 \(\mathcal T_u\) 内的链中不合法的上端点中深度最大的为 \(d\) 的 \(\mathcal T_u\) 内的方案数。
转移考虑子结点 \(v\) 向 \(u\) 合并:
然后前缀和优化一下,记 \(s_{u, d} = \sum\limits_{i=0}^d f_{u, d}\),则转移还可以写成:
时间复杂度 \(\mathcal O(n^2)\)。
我们并不会去给 \(1 \to fa_u\) 上的边赋值,所以有值的 \(f_{u, d}\) 的个数不会超过 \(sz_u\),用线段树合并来维护 \(f_u\),时间复杂度 \(\mathcal O(n \log n)\)。
代码
时代的眼泪
咕咕咕……