HAOI2015 简要题解
「HAOI2015」树上染色
题意
有一棵点数为 \(N\) 的树,树边有边权。给你一个在 \(0 \sim N\) 之内的正整数 \(K\),你要在这棵树中选择 \(K\) 个点,将其染成黑色,并将其他的 \(N-K\) 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间距离的和的收益。
问收益最大值是多少。
\(N \leq 2000, \ 0 \leq K \leq N\)
题解
树上距离,我们要么考虑差分深度,要么考虑一条边对于多少个点对进行贡献。
前者显然是不好考虑的,可以考虑后者。
令 \(f_{i, j}\) 为 \(i\) 子树内有 \(j\) 个黑点的最优答案。不难发现这个直接做一个树上背包即可,背包的权值就是这条边被多少对同色点对穿过乘上这条边的权值。
由于树上两点在 \(lca\) 贡献一次复杂度,那么复杂度是 \(\mathcal O(n ^ 2)\) 的。
代码
#include <bits/stdc++.h>
#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
using namespace std;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2124.in", "r", stdin);
freopen ("2124.out", "w", stdout);
#endif
}
const int N = 2010, M = N << 1;
using ll = long long;
int n, K;
ll dp[N][N], tmp[N];
int Head[N], Next[M], to[M], val[M], e = 0;
inline void add_edge(int u, int v, int w) {
to[++ e] = v; Next[e] = Head[u]; val[e] = w; Head[u] = e;
}
int sz[N];
void Dfs(int u, int fa = 0) {
for (int i = Head[u], v = to[i]; i; v = to[i = Next[i]])
if (v != fa) {
Dfs(v, u); Set(tmp, 0);
For (j, 0, min(K, sz[u])) For (k, 0, min(K, sz[v])) {
int Pair = k * (K - k) +
(sz[v] - k) * ((n - K) - (sz[v] - k));
chkmax(tmp[j + k], dp[u][j] + dp[v][k] + 1ll * Pair * val[i]);
}
Cpy(dp[u], tmp); sz[u] += sz[v];
}
Fordown (i, min(K, ++ sz[u]), 0)
dp[u][i] = max(i ? dp[u][i - 1] : 0ll, dp[u][i]);
}
int main () {
File();
n = read(); K = read();
For (i, 1, n - 1) {
int u = read(), v = read(), w = read();
add_edge(u, v, w); add_edge(v, u, w);
}
Dfs(1);
printf ("%lld\n", dp[1][K]);
return 0;
}
「HAOI2015」树上操作
题意
有一棵点数为 \(N\) 的树,以点 \(1\) 为根,且树有点权。然后有 \(M\) 个操作,分为三种:
- 把某个节点 \(x\) 的点权增加 \(a\) 。
- 把某个节点 \(x\) 为根的子树中所有点的点权都增加 \(a\) 。
- 询问某个节点 \(x\) 到根的路径中所有点的点权和。
\(N,M \leq 10^5\)
题解
树剖裸题,没什么好说的,复杂度是 \(\mathcal O(n \log^2 n)\) 。
代码
#include <bits/stdc++.h>
#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
using namespace std;
using ll = long long;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2125.in", "r", stdin);
freopen ("2125.out", "w", stdout);
#endif
}
const int N = 2e5 + 1e3;
int n;
#define lowbit(x) (x & -x)
template<int Maxn>
struct Fenwick_Tree {
ll sum1[Maxn], sum2[Maxn];
inline void Add(int pos, int uv) {
for (int tmp = pos; pos <= n; pos += lowbit(pos))
sum1[pos] += uv, sum2[pos] += 1ll * tmp * uv;
}
ll Sum(int pos) {
ll res = 0;
for (int tmp = pos; pos; pos -= lowbit(pos))
res += 1ll * (tmp + 1) * sum1[pos] - sum2[pos];
return res;
}
inline void Update(int ul, int ur, int val) {
Add(ul, val); if (ur < n) Add(ur + 1, -val);
}
inline ll Query(int ql, int qr) {
return Sum(qr) - Sum(ql - 1);
}
};
Fenwick_Tree<N> T;
int dep[N], fa[N]; vector<int> G[N];
int son[N], sz[N];
void Dfs_Init(int u, int from) {
sz[u] = 1;
dep[u] = dep[fa[u] = from] + 1;
for (int v : G[u]) if (v != from) {
Dfs_Init(v, u); sz[u] += sz[v];
if (sz[v] > sz[son[u]]) son[u] = v;
}
}
int dfn[N], efn[N], top[N];
void Dfs_Part(int u) {
static int clk = 0; dfn[u] = ++ clk;
top[u] = son[fa[u]] == u ? top[fa[u]] : u;
if (son[u]) Dfs_Part(son[u]);
for (int v : G[u]) if (v != son[u] && v != fa[u]) Dfs_Part(v);
efn[u] = clk;
}
int val[N];
int main () {
File();
n = read(); int m = read();
For (i, 1, n) val[i] = read();
For (i, 1, n - 1) {
int u = read(), v = read();
G[u].push_back(v); G[v].push_back(u);
}
Dfs_Init(1, 0); Dfs_Part(1);
For (i, 1, n) T.Update(dfn[i], dfn[i], val[i]);
For (i, 1, m) {
int opt = read(), x = read();
if (opt <= 2)
T.Update(dfn[x], opt == 1 ? dfn[x] : efn[x], read());
if (opt == 3) {
ll ans = 0;
for (int u = x; u; u = fa[top[u]])
ans += T.Query(dfn[top[u]], dfn[u]);
printf ("%lld\n", ans);
}
}
return 0;
}
「HAOI2015」数组游戏
题意
有一个长度为 \(N\) 的数组,甲乙两人在上面进行这样一个游戏:首先,数组上有一些格子是白的,有一些是黑的。然后两人轮流进行操作。每次操作选择一个白色的格子,假设它的下标为 \(x\) 。接着,选择一个大小在 \(1 \sim n / x\) 之间的整数 \(k\),然后将下标为 \(x\)、\(2x\)、...、\(kx\) 的格子都进行颜色翻转。不能操作的人输。
现在甲(先手)有一些询问。每次他会给你一个数组的初始状态,你要求出对于这种初始状态他是否有必胜策略。
\(N \leq 1000000000\),\(K,W \leq 100\)
题解
这是个经典的翻硬币游戏,可以参考一下 Candy? 的博客 。
每一个硬币可以看成独立的子游戏,所以:
局面的 \(SG\) 值为局面中每个正面朝上的棋子单一存在时的 \(SG\) 值的异或和。
令 \(SG(x)\) 为第 \(x\) 位为白的时候的 \(SG\) 值,那么有
这个直接暴力做是 \(\mathcal O(n \ln n)\) 的。
\(SG\) 函数通常考虑的优化就是找规律,打打表,不难发现:
\(\forall i, j\) 如果有 \(\displaystyle \lfloor \frac{n}{i} \rfloor = \lfloor \frac{n}{j} \rfloor\) 那么有 \(SG(i) = SG(j)\) 。
这个直接上整除分块就好了,然后枚举倍数的时候只需要枚举在分的块中的数,复杂度好像是 \(\mathcal O(n^{3/ 4})\) 的。
总结
翻硬币问题是一种经典的 nim
问题,可以设 \(SG\) 函数。
代码
#include <bits/stdc++.h>
#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
using namespace std;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2126.in", "r", stdin);
freopen ("2126.out", "w", stdout);
#endif
}
const int N = 1e5 + 1e3;
int n, d, k, sg[N], App[N];
int id1[N], id2[N], clk = 0;
#define id(x) (x <= d ? id1[x] : id2[n / (x)])
int main () {
File();
n = read();
d = sqrt(n + .5);
for (int i = 1; i <= n; i = n / (n / i) + 1) {
int x = n / i, v = 0; id(n / i) = App[0] = ++ clk;
for (int j = x << 1, nextj; j <= n; j = nextj + x) {
nextj = (n / (n / j)) / x * x;
App[v ^ sg[id(j)]] = clk;
if (((nextj - j) / x + 1) & 1) v ^= sg[id(j)];
}
while (App[sg[id(x)]] == clk) ++ sg[id(x)];
}
int cases = read();
while (cases --) {
int res = 0;
For (i, 1, read()) {
int x = read(); res ^= sg[id(x)];
}
puts(res ? "Yes" : "No");
}
return 0;
}
「HAOI2015」按位或
题意
刚开始你有一个数字 \(0\),每一秒钟你会随机选择一个 \([0,2^n-1]\) 的数字,与你手上的数字进行或操作。选择数字 \(i\) 的概率是 \(p[i]\) 问期望多少秒后,你手上的数字变成 \(2^n-1\)。
\(n \le 20\) 。
题解
似乎有强行推式子的做法。。。但是我好像不会 QAQ
但类似于这种无限期望,每个时刻会随机出现其中一些元素,到所有元素都出现的期望计算一般都可以用 \(\min-\max\) 容斥做。
即原问题是求集合中最晚的元素的期望,利用最值反演就变成求集合中最早元素的期望,即
然后有
我们就只需要对于每个 \(S\) 求 \(\sum_{T \cap S \not = \varnothing} P[T]\) 了。
这个直接取补集,考虑补集的子集和就行了,即
利用 \(\text{FMT}\) 就可以做到 $\mathcal O(n \times 2^n) $ 的复杂度。
总结
把 \(\min - \max\) 当做一个黑箱来用比较好。
代码
一开始蠢了,那个补集子集和没想到,利用容斥求得并不为空的贡献。。。
#include <bits/stdc++.h>
#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
using namespace std;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2127.in", "r", stdin);
freopen ("2127.out", "w", stdout);
#endif
}
const int N = 1 << 20;
const double eps = 1e-8;
int n, maxsta; double p[N], prob[N], sum[N];
void FMT(double *f) {
Rep (j, n) Rep (i, maxsta)
if (i >> j & 1) f[i] += f[i ^ (1 << j)];
}
int main () {
File();
n = read(); maxsta = 1 << n; int all = maxsta - 1;
int cur = 0;
Rep (i, maxsta) {
scanf ("%lf", &p[i]);
if (p[i] > eps) cur |= i;
}
if (cur != maxsta - 1) return puts("INF"), 0;
Rep (i, maxsta) sum[i] = p[all ^ i]; FMT(sum);
Rep (i, maxsta) if (i)
prob[i] = sum[all ^ i] * (__builtin_popcount(i) & 1 ? 1 : -1);
FMT(prob);
double ans = .0;
Rep (i, maxsta) if (i)
ans += (__builtin_popcount(i) & 1 ? 1 : -1) / prob[i];
printf ("%.7lf\n", ans);
return 0;
}
「HAOI2015」数字串拆分
题意
你有一个长度为 \(n\) 的数字串。定义 \(f(S)\) 为将 \(S\) 拆分成若干个 \(1\sim m\) 的数的和的方案数。
你可以将这个数字串分割成若干个数字(允许前导 \(0\) ),将他们加起来,求 \(f\) 的和。已知字符串和 \(m\) 后求答案,对 \(998244353\) 取模。
字符串长度不超过 \(500\),\(m \leq 5\)
题解
首先要解决的问题就是给定一个字符串 \(S\) 如何快速求 \(f(S)\) ,注意到 \(m\) 很小,而 \(S\) 很大,不难想到矩阵快速幂。
转移是很显然的 \(f(S) = \sum_{i = 1}^m f(S - i)\) ,搞个矩阵作为转移系数。
然后令 \(dp_i\) 为以 \(i\) 结尾的答案矩阵,转移显然 \(dp_i = \sum_{j < i} dp_j \times trans_{j \sim i}\) 。
我们唯一要处理的就是 \(trans_{j \sim i}\) ,可以利用二进制下矩阵快速幂解决,但显然高精度十进制转二进制很不方便。可以考虑直接预处理关于十进制的矩阵幂就行了。
复杂度是 \(\mathcal O(|S|^2m^3)\) 的。
总结
有时候不一定要纠结着二进制,可能没有那么好解决,十进制是一种不错的方案。
代码
#include <bits/stdc++.h>
#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
using namespace std;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2128.in", "r", stdin);
freopen ("2128.out", "w", stdout);
#endif
}
const int N = 510, M = 7, Mod = 998244353;
char str[N]; int m;
struct Matrix {
int a[M][M];
Matrix() { Set(a, 0); }
void Unit() { For (i, 1, m) a[i][i] = 1; }
inline Matrix friend operator * (const Matrix &lhs, const Matrix &rhs) {
Matrix res;
For (i, 1, m) For (k, 1, m) For (j, 1, m)
res.a[i][k] = (res.a[i][k] + 1ll * lhs.a[i][j] * rhs.a[j][k]) % Mod;
return res;
}
inline Matrix friend operator + (const Matrix &lhs, const Matrix &rhs) {
Matrix res;
For (i, 1, m) For (j, 1, m)
res.a[i][j] = (lhs.a[i][j] + rhs.a[i][j]) % Mod;
return res;
}
};
Matrix trans[N][10], Base, f[N];
int dp[N];
int main () {
File();
scanf ("%s", str + 1); m = read();
int n = strlen(str + 1);
For (i, 1, m - 1) Base.a[i][i + 1] = 1;
For (i, 1, m) Base.a[m][i] = 1;
trans[0][0].Unit(); trans[0][1] = Base;
For (i, 2, 9) trans[0][i] = trans[0][i - 1] * Base;
For (i, 1, n) {
trans[i][0].Unit();
trans[i][1] = trans[i - 1][1] * trans[i - 1][9];
For (j, 2, 9) trans[i][j] = trans[i][j - 1] * trans[i][1];
}
dp[0] = 1;
For (i, 1, 10) For (j, 1, min(i, m)) dp[i] += dp[i - j];
For (i, 1, m) f[0].a[i][1] = dp[i - 1];
For (i, 1, n) {
Matrix now; now.Unit();
Fordown (j, i, 1) {
now = now * trans[i - j][str[j] ^ 48];
f[i] = f[i] + now * f[j - 1];
}
}
printf ("%d\n", f[n].a[1][1]);
return 0;
}