deaf的杂题选讲
放个deaf语录
deaf:建议大家平时练习找题不要找太简单的,比如CF就找3000+,洛谷的话就选黑紫题就好了
注: 在座的有新初三的不到省一水平的同学。
[TJOI2019]甲苯先生的线段树
这题题面简直就是刻在DNA里的点分治(路径为某个数的路径条数)。
我们考虑借鉴点分治的思想,枚举 lca,然后在考虑两个子树各引出一条链拼接起来。
然而发现十分的不可做。
如果想使得 \(p\) 的左右儿子分别引出了一条长度为 \(i,j\) 的链(链分别从左儿子和右儿子开始算起),且这两条链加上 \(p\) 的编号和为 \(s\),那么注意到若 \(s,i,j\) 已知,我们可以分别讨论这两条链全往左儿子走和全往右儿子走的编号和,那么 \(s\) 一定在这两者之间。解一下发现 \(p\) 可以直接唯一确定。(目前还是没搞懂这玩意儿是怎么注意到的)。
剩下的就非常简单了。枚举 \(i,j\) 后考虑在每个深度往左和往右对答案的贡献,不难发现我们就是要用 \(2-1,2^2-1...2^{i-1}-1,2-1,2^2-1...2^{j-1}-1\) 凑出一个数。枚举用了多少个数转化为数位 dp 模板就行了。
复杂度 \(O(n^5)\),但上界非常松,跑得飞快。
#include <cstdio>
#include <algorithm>
#include <cstring>
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
typedef long long ll;
int Log(ll n) {
int x = 0;
while (n != 1) n >>= 1, ++ x;
return x;
}
ll getdis(ll a, ll b) {
if (Log(a) < Log(b)) a ^= b ^= a ^= b;
int tmp = Log(a) - Log(b);
ll ans = 0;
while (tmp --) ans += a, a >>= 1;
if (a == b) return ans + a;
while ((a >> 1) != (b >> 1)) ans += a + b, a >>= 1, b >>= 1;
return ans + a + b + (a >> 1);
}
ll dp[55][105][2], c[105];
ll solve(ll n, int a, int b) {
if (!n) return 1;
if (a < 0) a = 0;
if (b < 0) b = 0;
memset(c, 0, sizeof c);
for (int i = 1; i <= a; ++ i) ++ c[i];
for (int i = 1; i <= b; ++ i) ++ c[i];
ll ans = 0;
for (int k = 0; k <= a + b; ++ k) if (!(n + k & 1ll)) {
int lim = Log(n + k);
dp[0][0][0] = 1;
for (int i = 1; i <= lim; ++ i)
for (int j = 0; j <= 2 * i && j <= a + b; ++ j) {
dp[i][j][0] = dp[i][j][1] = 0;
if (n + k & 1ll << i) {
dp[i][j][0] = dp[i - 1][j][1];
if (c[i] && j >= 1) dp[i][j][0] += dp[i - 1][j - 1][0] * c[i];
if (c[i] == 2 && j >= 2) dp[i][j][1] = dp[i - 1][j - 2][1];
} else {
dp[i][j][0] = dp[i - 1][j][0];
if (c[i] && j >= 1) dp[i][j][1] = dp[i - 1][j - 1][1] * c[i];
if (c[i] >= 2 && j >= 2) dp[i][j][1] += dp[i - 1][j - 2][0];
}
}
ans += dp[lim][k][0];
}
return ans;
}
int main() {
int _;
scanf("%d", &_);
while (_ --) {
int n, type;
ll a, b, s = 0;
scanf("%d%lld%lld%d", &n, &a, &b, &type);
s = getdis(a, b);
if (type == 1) {printf("%lld\n", s); continue;}
ll ans = 0;
for (int i = 0; i < n; ++ i)
for (int j = 0; j < n; ++ j) {
ll p = (s + 1 - (1ll << j)) / ((1ll << i + 1) + (1ll << j + 1) - 3);
if (p <= 0) break;
if (Log(p) + max(i, j) >= n) continue;
if (((1ll << i + 1) + (1ll << j + 1) - 3) * p + (1ll << j + 1) - 3 - i - j + (1ll << i) < s) continue;
ans += solve(s - ((1ll << i + 1) + (1ll << j + 1) - 3) * p - (1ll << j) + 1, i - 1, j - 1);
}
printf("%lld\n", ans - 1);
}
}
[十二省联考 2019] 皮配
如果没有学校有偏好,就可以把阵营与派系分开考虑。
对于有偏好的学校,注意到可以首先确定无限制学校城市的阵营与派系,然后确定无限制学校的派系,这些都可以 \(O(nm)\) 地完成。再用暴力 dp 算出被限制学校的方案即可。总复杂度 \(O(nm+mk^2s)\)。
其实是个很板的背包
#include <cstdio>
#include <cstring>
#include <vector>
const int mod = 998244353;
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
inline void add(int &x, const int y) {
if ((x += y) >= mod) x -= mod;
}
int f[2505], g[2505], b[1005], s[1005], dislike[1005], sum[2505][2505], dp[2505][305], tmp[2505][305];
std::vector<int> sch[1005];
std::vector<std::pair<int, int> > dsch[1005];
inline int getsum(int x1, int y1, int x2, int y2) {
x1 = max(x1, 0), y1 = max(y1, 0);
if (x1 > x2 || y1 > y2) return 0;
++ x1, ++ y1, ++ x2, ++ y2;
return (1ll * sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]) % mod;
}
int main() {
int _;
scanf("%d", &_);
while (_ --) {
memset(f, 0, sizeof f);
memset(g, 0, sizeof g);
memset(dp, 0, sizeof dp);
memset(dislike, -1, sizeof dislike);
int n, m, c0, c1, d0, d1, tot = 0, ans = 0;
scanf("%d%*d%d%d%d%d", &n, &c0, &c1, &d0, &d1);
m = max(c0, max(c1, max(d0, d1)));
for (int i = 1; i <= n; ++ i) sch[i].clear(), dsch[i].clear();
for (int i = 1; i <= n; ++ i) scanf("%d%d", b + i, s + i), tot += s[i];
int k;
scanf("%d", &k);
for (int i = 1; i <= k; ++ i) {
int id, p;
scanf("%d%d", &id, &p);
dislike[id] = p;
}
for (int i = 1; i <= n; ++ i)
if (dislike[i] == -1) sch[b[i]].push_back(s[i]);
else dsch[b[i]].push_back(std::make_pair(s[i], dislike[i]));
f[0] = g[0] = 1;
for (int i = 1; i <= n; ++ i)
if (dsch[i].empty()) {
if (sch[i].empty()) continue;
int sum = 0;
for (int j : sch[i]) {
sum += j;
for (int k = d0; k >= j; -- k) add(g[k], g[k - j]);
}
for (int k = c0; k >= sum; -- k) add(f[k], f[k - sum]);
} else
for (int j : sch[i])
for (int k = d0; k >= j; -- k) add(g[k], g[k - j]);
dp[0][0] = 1;
int tot2 = 0;
for (int i = 1, now = 0; i <= n; ++ i) if (dsch[i].size()) {
int sum = 0, sum2 = 0;
for (int j : sch[i]) sum += j;
for (auto j : dsch[i]) sum += j.first, sum2 += j.first;
now += sum, tot2 += sum2;
int limc = min(c0, now), limd = min(d0, tot2);
for (int j = 0; j < sum; ++ j) memset(tmp[j], 0, limd + 2 << 2);
for (int j = sum; j <= limc; ++ j) memcpy(tmp[j], dp[j - sum], limd + 2 << 2);
for (auto j : dsch[i])
for (int x = limc; x >= 0; -- x)
for (int y = limd; y >= 0; -- y) {
if (j.second == 3) dp[x][y] = 0;
if (j.second != 2 && y >= j.first) add(dp[x][y], dp[x][y - j.first]);
}
for (auto j : dsch[i])
for (int x = limc; x >= sum; -- x)
for (int y = limd; y >= 0; -- y) {
if (j.second == 1) tmp[x][y] = 0;
if (j.second != 0 && y >= j.first) add(tmp[x][y], tmp[x][y - j.first]);
}
for (int i = 0; i <= limc; ++ i)
for (int j = 0; j <= limd; ++ j) add(dp[i][j], tmp[i][j]);
}
for (int i = 1; i <= m + 1; ++ i)
for (int j = 1; j <= m + 1; ++ j)
sum[i][j] = (1ll * sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + 1ll * f[i - 1] * g[j - 1]) % mod;
for (int i = 0; i <= c0 && i <= tot; ++ i)
for (int j = 0; j <= d0 && j <= tot2; ++ j) if (dp[i][j])
ans = (ans + 1ll * dp[i][j] * getsum(tot - c1 - i, tot - d1 - j, c0 - i, d0 - j)) % mod;
printf("%d\n", (ans + mod) % mod);
}
return 0;
}
[HNOI2019]校园旅行
神仙题。
在一个回文串两端加上同等数量的 1 可以变成另一个回文串。而一条边可以重复经过,所以如果左边可以加两个 \(1\),右边可以加 \(4\) 个 \(1\),那我只需要再左边多走一遍就可以使得两边加的 \(1\) 个数一样了。
这提示我们只需要考虑 \(1\) 个数的奇偶性。
暴论:看到奇偶性,直接想到二分图。
考虑把两端都是 \(1\) 的点的边全部提出来,对于它们构成的每个连通块,如果这个连通块是一个二分图,不管怎么走,则只需要保留一颗生成树。正确性显然。如果不是二分图,那么不管走奇数还是偶数条边,我都可以随意跳到图上的任何一个点,所以我们加一个自环即可。
两端都是 \(0\) 的同理。两端不同的由于建出来一定是一个二分图,根本不需要考虑上面的东西,直接保留一颗生成树即可。
这样最多只有 \(3n\) 条无向边。大力 bfs 即可。
#include <cstdio>
#include <numeric>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1 ++)
char buf[100000], *p1, *p2;
inline int read() {
char ch;
int x = 0;
while ((ch = gc) < 48);
do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
return x;
}
char str[5005];
struct Edge {int to, nxt;} e[30005];
struct node {int x, y;} Q[12505005];
int head[5005], hd, tl, n, m, q, tot;
bool mark[5005][5005];
inline void AddEdge(int u, int v) {
e[++ tot].to = v, e[tot].nxt = head[u], head[u] = tot;
}
struct Connector {
int from[500005], to[500005], fa1[5005], fa2[10005], cnt;
bool mark[500005];
int find1(int x) {return fa1[x] == x ? x : fa1[x] = find1(fa1[x]);}
int find2(int x) {return fa2[x] == x ? x : fa2[x] = find2(fa2[x]);}
inline void merge1(int u, int v) {
u = find1(u), v = find1(v);
if (u != v) fa1[u] = v;
}
inline void merge2(int u, int v) {
u = find2(u), v = find2(v);
if (u != v) fa2[u] = v;
}
inline void add(int u, int v) {from[++ cnt] = u, to[cnt] = v;}
void connect() {
std::iota(fa1 + 1, fa1 + n + 1, 1);
for (int i = 1; i <= cnt; ++ i)
if (find1(from[i]) != find1(to[i]))
merge1(from[i], to[i]), AddEdge(from[i], to[i]), AddEdge(to[i], from[i]);
std::iota(fa2 + 1, fa2 + 2 * n + 1, 1);
for (int i = 1; i <= cnt; ++ i) if (!mark[find1(from[i])]) {
if (find2(from[i]) == find2(to[i])) mark[find1(from[i])] = true, AddEdge(from[i], from[i]);
merge2(from[i], to[i] + n), merge2(from[i] + n, to[i]);
}
}
} b, w, bw;
void bfs() {
hd = 1, tl = 0;
for (int i = 1; i <= n; ++ i) mark[i][i] = true, Q[++ tl] = node{i, i};
for (int i = 1; i <= n; ++ i)
for (int j = head[i]; j; j = e[j].nxt)
if (i < e[j].to && !mark[i][e[j].to] && str[i] == str[e[j].to])
mark[i][e[j].to] = true, Q[++ tl] = node{i, e[j].to};
while (hd <= tl) {
int x = Q[hd].x, y = Q[hd].y;
++ hd;
for (int i = head[x]; i; i = e[i].nxt)
for (int j = head[y]; j; j = e[j].nxt) if (str[e[i].to] == str[e[j].to]) {
int u = e[i].to, v = e[j].to;
if (u > v) u ^= v ^= u ^= v;
if (!mark[u][v]) mark[u][v] = true, Q[++ tl] = node{u, v};
}
}
}
int main() {
scanf("%d%d%d%s", &n, &m, &q, str + 1);
for (int i = 1, u, v; i <= m; ++ i) {
u = read(), v = read();
if (str[u] == '0' && str[v] == '0') w.add(u, v);
else if (str[u] == '1' && str[v] == '1') b.add(u, v);
else bw.add(u, v);
}
b.connect(), w.connect(), bw.connect();
bfs();
while (q --) {
int u = read(), v = read();
if (u > v) u ^= v ^= u ^= v;
puts(mark[u][v] ? "YES" : "NO");
}
return 0;
}
CF1517F Reunion
如果能求半径不超过 \(r\) 的方案数,就可以得到半径恰好为 \(r\) 的方案数。
对于半径不超过 \(r\) 这个条件,我们考虑将限制条件转为所有没空的节点与它距离不超过 \(r\) 的点覆盖了整棵树。
注意到,当子树内有点没有被覆盖的时候,子树内的某些点即使能覆盖到子树外的点,也是可以被其它点取代的。因此状态记下当前最深的没有被覆盖的节点/最浅的没空的节点,然后树形背包随便数。
#include <cstdio>
#include <cstring>
const int mod = 998244353, inv2 = 499122177;
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
struct Edge {int to, nxt;} e[605];
int head[305], f[305][305], g[305][305], tmpf[305], tmpg[305], a[305], dep[305], tot, r;
inline void AddEdge(int u, int v) {
e[++ tot].to = v, e[tot].nxt = head[u], head[u] = tot;
}
void dfs(int u, int fa) {
f[u][0] = g[u][0] = 1, dep[u] = 0;
for (int i = head[u]; i; i = e[i].nxt) if (e[i].to != fa) {
dfs(e[i].to, u);
int v = e[i].to;
memset(tmpf, 0, sizeof tmpf);
memset(tmpg, 0, sizeof tmpg);
for (int j = 0; j <= dep[u]; ++ j)
for (int k = 0; k <= dep[v]; ++ k) {
tmpf[max(j, k + 1)] = (tmpf[max(j, k + 1)] + 1ll * f[u][j] * f[v][k]) % mod;
if (j + k + 1 <= r) {
tmpg[k + 1] = (tmpg[k + 1] + 1ll * f[u][j] * g[v][k]) % mod;
tmpg[j] = (tmpg[j] + 1ll * g[u][j] * f[v][k]) % mod;
} else {
tmpf[j] = (tmpf[j] + 1ll * f[u][j] * g[v][k]) % mod;
tmpf[k + 1] = (tmpf[k + 1] + 1ll * g[u][j] * f[v][k]) % mod;
}
tmpg[min(j, k + 1)] = (tmpg[min(j, k + 1)] + 1ll * g[u][j] * g[v][k]) % mod;
}
dep[u] = max(dep[u], dep[e[i].to] + 1);
memcpy(f[u], tmpf, sizeof f[u]), memcpy(g[u], tmpg, sizeof g[u]);
}
}
int main() {
int n, ans = 0;
scanf("%d", &n);
for (int i = 1, u, v; i < n; ++ i)
scanf("%d%d", &u, &v), AddEdge(u, v), AddEdge(v, u);
for (r = 1; r < n; ++ r) {
memset(f, 0, sizeof f), memset(g, 0, sizeof g);
dfs(1, -1);
for (int i = 0; i < n; ++ i) ans = (ans + f[1][i]) % mod;
}
for (int i = 1; i <= n; ++ i) ans = 1ll * ans * inv2 % mod;
printf("%d", ans);
return 0;
}
「ZJOI2019 Day1」线段树
显然我们只需要算每个节点 tag 期望之和就行了。设节点的 tag 期望为 \(p\)。
那么分情况讨论:
- 完全被 \([l,r]\) 覆盖的点。\(p\rightarrow (p+1)/2\)。
- 与 \([l,r]\) 有交的点。\(p\rightarrow p/2\)。
- 与 \([l,r]\) 无交且父亲与 \([l,r]\) 有交的点。……只要祖先有一个的 tag 是 \(1\),就会一直下放到它这个位置。
那我们直接记下 \(q\) 表示跟到它的路上有节点 tag 为 \(1\) 的概率。看起来很粗暴但没准行得通。
- 完全被 \([l,r]\) 覆盖的点。\(p\rightarrow (p+1)/2\)。子树内所有节点 \(q\rightarrow (q+1)/2\)。
- 与 \([l,r]\) 有交的点。\(p\rightarrow p/2,q\rightarrow q/2\)。
- 与 \([l,r]\) 无交且父亲与 \([l,r]\) 有交的点。\(p\rightarrow (p+q)/2\),\(q\) 不变。
竟然可以成功转移了,然后就没了。
#include <cstdio>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1 ++)
const int mod = 998244353, inv2 = 499122177;
inline void add(int &x, const int y) {
if ((x += y) >= mod) x -= mod;
}
inline void mns(int &x, const int y) {
if ((x -= y) < 0) x += mod;
}
inline void div2(int &x) {
if (x & 1) x = (x >> 1) + inv2;
else x >>= 1;
}
inline void adddiv(int &x) {
if (x & 1) x = x + 1 >> 1;
else x = (x >> 1) + inv2;
}
char buf[100000], *p1, *p2;
inline int read() {
char ch;
int x = 0;
while ((ch = gc) < 48);
do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
return x;
}
struct node {
int l, r, p, q, add, mul;
} tree[400005];
int ans, t = 1;
void build(int p, int l, int r) {
tree[p].l = l, tree[p].r = r, tree[p].mul = 1;
if (l != r) {
build(p << 1, l, l + r >> 1);
build(p << 1 | 1, (l + r >> 1) + 1, r);
}
}
inline void pushdown(int p) {
if (tree[p].mul == 1 && tree[p].add == 0) return;
tree[p << 1].q = (1ll * tree[p << 1].q * tree[p].mul + tree[p].add) % mod;
tree[p << 1 | 1].q = (1ll * tree[p << 1 | 1].q * tree[p].mul + tree[p].add) % mod;
tree[p << 1].add = (1ll * tree[p << 1].add * tree[p].mul + tree[p].add) % mod;
tree[p << 1 | 1].add = (1ll * tree[p << 1 | 1].add * tree[p].mul + tree[p].add) % mod;
tree[p << 1].mul = 1ll * tree[p << 1].mul * tree[p].mul % mod;
tree[p << 1 | 1].mul = 1ll * tree[p << 1 | 1].mul * tree[p].mul % mod;
tree[p].mul = 1, tree[p].add = 0;
}
void update(int p, int l, int r) {
if (l <= tree[p].l && tree[p].r <= r) {
mns(ans, tree[p].p), adddiv(tree[p].p), add(ans, tree[p].p), adddiv(tree[p].q);
div2(tree[p].mul), adddiv(tree[p].add);
return;
}
pushdown(p);
mns(ans, tree[p].p), div2(tree[p].p), add(ans, tree[p].p), div2(tree[p].q);
int mid = tree[p].l + tree[p].r >> 1;
if (l <= mid) update(p << 1, l, r);
else {
mns(ans, tree[p << 1].p);
add(tree[p << 1].p, tree[p << 1].q), div2(tree[p << 1].p);
add(ans, tree[p << 1].p);
}
if (mid < r) update(p << 1 | 1, l, r);
else {
mns(ans, tree[p << 1 | 1].p);
add(tree[p << 1 | 1].p, tree[p << 1 | 1].q), div2(tree[p << 1 | 1].p);
add(ans, tree[p << 1 | 1].p);
}
}
int main() {
int n = read(), q = read();
build(1, 1, n);
while (q --) {
int op = read();
if (op == 1) {
int l = read(), r = read();
update(1, l, r), add(t, t);
} else printf("%d\n", (int)(1ll * ans * t % mod));
}
return 0;
}