2022 暑假水题选做
20220627
P3914 染色计数
思路:考虑树形 dp。设 \(f_{u,i}\) 为考虑以 \(u\) 为根的子树且 \(u\) 染上 \(i\) 时的答案,则:
后面的 \(\sum\) 可以通过维护 \(\sum_{i=1}^m f_{u,i}\) 来优化掉。
算法:树形 dp。
技巧:通过处理某些东西优化复杂度。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 5000, P = 1e9 + 7;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
int n, m, f[N + 10][N + 10], sum[N + 10];
void dfs(int u, int _fa) {
for (int _ = head[u]; _; _ = e[_].nxt) {
int v = e[_].to;
if (v == _fa) continue;
dfs(v, u);
for (int i = 1; i <= m; i++)
f[u][i] = 1LL * f[u][i] * ((sum[v] - f[v][i]) % P) % P;
}
for (int i = 1; i <= m; i++)
(sum[u] += f[u][i]) %= P;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x);
while (x--) {
int y; scanf("%d", &y);
f[i][y] = 1;
}
}
for (int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
addEdge(u, v), addEdge(v, u);
}
dfs(1, 0);
printf("%d\n", (sum[1] % P + P) % P);
return 0;
}
CF1677C Tokitsukaze and Two Colorful Tapes
思路:先拆置换,对于每个置换,填法一定是 \(\max,\min,\max,\min,\cdots\)。对于一个长度为 \(m\) 的置换,“山峰”有 \(\left\lfloor\frac m2\right\rfloor\) 个,“山谷”有 \(m-\left\lfloor\frac m2\right\rfloor\) 个,而每个山峰 \(x\) 对答案的贡献为 \(2x\),每个山谷 \(x\) 对答案的贡献为 \(-2x\)。那么当我们让最大的数做山峰时答案有最大值 \(2\sum\left\lfloor\frac m2\right\rfloor(n-\sum\left\lfloor\frac m2\right\rfloor)\)。
算法:贪心。
技巧:拆置换、分别考虑每个数对答案的贡献。
想到了的:拆置换、贪心。
没想到的:考虑贡献。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 1e5;
int n, a[N + 10], b[N + 10], pos[N + 10];
bool vis[N + 10];
void mian() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
for (int i = 1; i <= n; i++)
scanf("%d", b + i), pos[b[i]] = i;
int peak = 0;
for (int i = 1; i <= n; i++) {
int x = i, cnt = 0;
while (1) {
if (vis[x]) break;
vis[x] = 1;
cnt++;
x = pos[a[x]];
}
peak += cnt / 2;
}
printf("%lld\n", 2LL * peak * (n - peak));
}
int main() {
int T; scanf("%d", &T);
while (T--) {
for (int i = 1; i <= n; i++)
a[i] = b[i] = pos[i] = vis[i] = 0;
mian();
}
return 0;
}
CF1665D GCD Guess
思路:考虑分别算出每个二进制位上的数字。假设我们知道了第 \(0\sim k-1\) 位为 \(r\),则当 \(\gcd(x+2^k-r,2^{k+1})=\gcd(x+2^k-r,x+2^k-r+2^{k+1})=2^{k+1}\) 时第 \(k\) 位为 \(1\)(构造方法就是把第 \(0\sim k-1\) 位去了然后给第 \(k\) 位加一)。
算法:gcd、位运算。
技巧:按位考虑、构造。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
int query(int a, int b) {
printf("? %d %d\n", a, b);
fflush(stdout);
int x; scanf("%d", &x);
return x;
}
void submit(int x) {
printf("! %d\n", x);
fflush(stdout);
}
void mian() {
int ans = 0;
for (int i = 0; i < 30; i++) {
if (query((1 << i) - ans, (1 << (i + 1)) + (1 << i) - ans) == (1 << (i + 1)))
ans |= (1 << i);
}
submit(ans);
}
int main() {
int T; scanf("%d", &T);
while (T--) mian();
return 0;
}
20220628
CF1696D Permutation Graph
思路:考虑贪心:每次都尽可能往右走。在当前位于 \(a_i\) 时,如果 \(a_i\lt a_{i+1}\),则 \(a_i\) 一定是作为最小值的,此时不断的往右跳到离他最近的比他大的数(可以用单调栈处理),直到这个区间包含比 \(a_i\) 小的数为止;如果 \(a_i\gt a_{i+1}\) 则同理。
算法:贪心。
技巧:单调栈预处理。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <stack>
using namespace std;
#define eprintf(...) fprintf(stderr, __VA_ARGS__)
const int N = 2.5e5;
int n, a[N + 10], pre[N + 10], suc[N + 10];
void mian() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
stack<int> stk;
stk.push(n + 1);
for (int i = n; i >= 1; i--) {
while (stk.size() > 1 && a[stk.top()] < a[i]) stk.pop();
suc[i] = stk.top();
stk.push(i);
}
while (!stk.empty()) stk.pop();
stk.push(n + 1);
for (int i = n; i >= 1; i--) {
while (stk.size() > 1 && a[stk.top()] > a[i]) stk.pop();
pre[i] = stk.top();
stk.push(i);
}
for (int i = 1; i <= n; i++)
eprintf("de: %d %d\n", pre[i], suc[i]);
eprintf("\n");
int now = 1, ans = 0;
while (now < n) {
if (a[now] < a[now + 1]) {
int lim = pre[now] - 1;
while (suc[now] <= lim) now = suc[now];
} else {
int lim = suc[now] - 1;
while (pre[now] <= lim) now = pre[now];
}
ans++;
}
printf("%d\n", ans);
}
int main() {
int T; scanf("%d", &T);
while (T--) {
for (int i = 1; i <= n; i++)
a[i] = pre[i] = suc[i] = 0;
mian();
}
return 0;
}
CF1695C Zero Path
思路:设 \(f_{i,j,k}\) 为是否存在 \(a_{1,1}\sim a_{n,m}\) 且和为 \(k\) 的路径,则
可以用 std::bitset 优化。
算法:dp。
技巧:压位优化 dp。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <bitset>
using namespace std;
const int N = 1e3;
int n, m, a[N + 10][N + 10];
bitset<2001> f[N + 10][N + 10]; // -1000 ~ 1000
void mian() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", a[i] + j);
f[0][1][1001] = f[1][0][1001] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
if (a[i][j] == 1)
f[i][j] = (f[i - 1][j] << 1) | (f[i][j - 1] << 1);
else
f[i][j] = (f[i - 1][j] >> 1) | (f[i][j - 1] >> 1);
}
puts(f[n][m][1001] == 1 ? "YES" : "NO");
}
int main() {
int T; scanf("%d", &T);
while (T--) {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
a[i][j] = 0, f[i][j].reset();
mian();
}
return 0;
}
CF1696E Placing Jinas
思路:打表找规律,发现把 \((i,j)\) 处理掉的步数为 \({i+j\choose j}\),于是答案为 \(\sum_{i=0}^n\sum_{j=0}^{a_i-1}{i+j\choose j}\),根据 \({n+r+1\choose r}={n+r\choose r}+{n+r-1\choose r-1}+\cdots+{n\choose 0}\) 得 \(ans=\sum_{i=0}^n{i+a_i\choose a_i-1}\)。
算法:组合数。
技巧:找规律、推式子。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 2e5, P = 1e9 + 7;
int n, a[N + 10], fac[N * 2 + 10], ifac[N * 2 + 10];
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1LL * a * res % P;
a = 1LL * a * a % P; b >>= 1;
}
return res;
}
void init() {
fac[0] = 1;
for (int i = 1; i <= N * 2; i++) fac[i] = 1LL * fac[i - 1] * i % P;
ifac[N * 2] = qpow(fac[N * 2], P - 2);
for (int i = 2 * N - 1; i >= 0; i--) ifac[i] = 1LL * ifac[i + 1] * (i + 1) % P;
}
int C(int a, int b) {
if (a < b) return 0;
return 1LL * fac[a] * ifac[b] % P * ifac[a - b] % P;
}
int main() {
scanf("%d", &n);
for (int i = 0; i <= n; i++)
scanf("%d", a + i);
init();
int ans = 0;
for (int i = 0; i <= n; i++)
(ans += C(i + a[i], a[i] - 1)) %= P;
printf("%d\n", ans);
return 0;
}
CF1696C Fishingprince Plays With Array
思路:观察到两个操作互为逆向操作,考虑找到一个中间状态。对于本题,中间状态可以选择拆分到不能拆时的数列。
算法:?
技巧:考虑找中间状态。
想到了的:两个操作互为逆向操作。
没想到的:考虑找中间状态。
代码
#include <algorithm>
#include <cstdio>
#include <functional>
#include <vector>
using namespace std;
typedef long long ll;
int main() {
int T; scanf("%d", &T);
while (T--) {
int n, m, k;
scanf("%d%d", &n, &m);
vector<int> a(n);
for (auto &x : a) scanf("%d", &x);
scanf("%d", &k);
vector<int> b(k);
for (auto &x : b) scanf("%d", &x);
auto split = [&](vector<int> vec) {
vector<pair<int, ll>> res;
for (auto x : vec) {
int cnt = 1;
while (x % m == 0) cnt *= m, x /= m;
if (!res.empty() && res.back().first == x) res.back().second += cnt;
else res.push_back({x, cnt});
}
return res;
};
if (split(a) == split(b)) puts("Yes");
else puts("No");
}
return 0;
}
20220630
CF1698E PermutationForces II
思路:考虑到第 \(i\) 次操作必定为把 \(a_i\) 放到它应该放到的位置(这样一定不会更差),可以猜出结论:有方案当且仅当 \(a_i-b_i\le s\)。把这个式子变成 \(a_i-s\le b_i\) 后就可以做了。
算法:计数。
技巧:观察性质、猜结论(?)。
想到了的:无。
没想到的:都没想到。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 2e5, P = 998244353;
int n, s;
pair<int, int> a[N + 10];
bool vis[N + 10];
int b[N + 10], c[N + 10];
void mian() {
scanf("%d%d", &n, &s);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i].first);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].second);
if (a[i].second != -1) vis[a[i].second] = 1;
}
int m = 0;
for (int i = 1; i <= n; i++)
if (!vis[i]) b[++m] = i;
int mx = 0;
for (int i = 1; i <= n; i++) {
if (a[i].second != -1) mx = max(mx, a[i].first - a[i].second);
}
if (mx > s) return puts("0"), void();
int totc = 0;
for (int i = 1; i <= n; i++) {
if (a[i].second == -1)
c[++totc] = b + m + 1 - lower_bound(b + 1, b + m + 1, a[i].first - s);
}
sort(c + 1, c + m + 1);
int ans = 1;
for (int i = 1; i <= m; i++)
ans = 1LL * ans * (c[i] - (i - 1)) % P;
printf("%d\n", ans);
}
int main() {
int T; scanf("%d", &T);
while (T--) {
for (int i = 1; i <= n; i++) vis[i] = 0;
mian();
}
return 0;
}
20220705
CF938D Buy a Ticket
思路:考虑建一个新点 \(0\),把它和每个 \(1\le i\le n\) 连边,边权为 \(a_i\)。以 \(0\) 为起点跑最短路即可。
算法:最短路。
技巧:建新点、把路径倒过来考虑。
想到了的:(课上例题)
没想到的:(课上例题)
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 2e5, M = 5e5;
struct Edge {
int to, nxt; ll w;
} e[M * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v, ll w) {
e[++tote] = {v, head[u], w};
head[u] = tote;
}
int n, m;
ll dis[N + 10];
bool vis[N + 10];
void dijkstra() {
memset(dis, 0x3f, sizeof(dis)); dis[0] = 0;
priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<pair<ll, int>>> q;
q.push({dis[0], 0});
while (!q.empty()) {
auto _ = q.top(); q.pop();
int u = _.second;
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
q.push({dis[v], v});
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; ll w;
scanf("%d%d%lld", &u, &v, &w);
w *= 2;
addEdge(u, v, w), addEdge(v, u, w);
}
for (int i = 1; i <= n; i++) {
ll x; scanf("%lld", &x);
addEdge(0, i, x), addEdge(i, 0, x);
}
dijkstra();
for (int i = 1; i <= n; i++)
printf("%lld%c", dis[i], " \n"[i == n]);
return 0;
}
20220709
CF1701C Schedule Management
思路:二分答案 \(x\),看在 \(x\) 时间里能不能完成 \(\ge m\) 项任务。具体的 check 方法就是统计每个人有多少项任务可以在 \(1\) 单位时间内完成(把它叫做 \(cnt_i\)),对于每个人 \(i\),如果 \(x\ge cnt_i\),那么他在做完 \(cnt_i\) 个任务之后还能做一些,否则就只能做 \(x\) 个。
算法:二分答案、贪心。
技巧:观察到能搞出来的时间是连续的。
想到了的:无。
没想到的:都没想到。
代码
const int N = 2e5;
int n, m, a[N + 10], cnt[N + 10];
bool check(int x) {
ll y = 0;
for (int i = 1; i <= n; i++) {
if (x >= cnt[i]) y += 0LL + cnt[i] + (x - cnt[i]) / 2;
else y += 0LL + x;
}
return y >= m;
}
void mian() {
for (int i = 1; i <= n; i++)
cnt[i] = 0;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
scanf("%d", a + i), cnt[a[i]]++;
int l = 0, r = 2 * m + 1;
while (l < r) {
int mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l);
}
CF1701D Permutation Restoration
思路:题目中的条件是 \(\left\lfloor\frac i{a_i}\right\rfloor=b_i\),也就是说 \(a_i\) 必须在 \(\left[\left\lfloor\frac i{b_i+1}\right\rfloor+1,\left\lfloor\frac i{b_i}\right\rfloor\right]\) 这个范围内。这样题目就转化为了区间问题。可以贪心解决:将区间按右端点排序,然后用 std::set 维护还没有配上对的 \(a_i\)(不是区间!),按顺序依次取出一个区间 \([l_i,r_i]\),然后贪心的选择 std::set 里比 \(l_i\) 大的最小的数和这个区间匹配。
算法:贪心。
技巧:std::set、关于线段的贪心。
想到了的:卡范围、排序、贪心。
没想到的:数据结构维护(std::set)。
代码
const int N = 5e5;
struct Node {
int l, r, id;
};
int n, a[N + 10], b[N + 10];
Node seg[N + 10];
void mian() {
for (int i = 1; i <= n; i++)
a[i] = b[i] = 0, seg[i].l = seg[i].r = seg[i].id = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", b + i);
for (int i = 1; i <= n; i++)
if (b[i] == 0) seg[i] = {i + 1, n, i};
else seg[i] = {i / (b[i] + 1) + 1, i / b[i], i};
sort(seg + 1, seg + n + 1, [](const Node &x, const Node &y) {
return x.r == y.r ? x.l < y.l : x.r < y.r;
});
set<int> s;
for (int i = 1; i <= n; i++) s.insert(i);
for (int i = 1; i <= n; i++) {
auto it = s.lower_bound(seg[i].l);
a[seg[i].id] = *it;
s.erase(it);
}
for (int i = 1; i <= n; i++)
printf("%d%c", a[i], " \n"[i == n]);
}
20220717
P2899 [USACO08JAN]Cell Phone Network G
思路:设 \(f_{u,0/1/2}\) 表示考虑以 \(u\) 为根的子树,且 \(u\) 被自己/父亲/儿子管辖时的答案,则:
但是对于 \(f_{u,2}\),还需要考虑所有儿子都是 \(f_{v,2}\) 的情况,这时需要把最小的 \(f_{v,0}\) 补回来。
算法:树形 dp。
技巧:无。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 10000;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
// f[u][0] -> self
// f[u][1] -> parent
// f[u][2] -> child
int n, f[N + 10][3];
void dfs(int u, int _fa) {
bool flg = 0;
int add = 0x3f3f3f3f;
f[u][0] = 1;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == _fa) continue;
dfs(v, u);
f[u][0] += min({f[v][0], f[v][1], f[v][2]});
f[u][1] += min(f[v][0], f[v][2]);
if (f[v][0] <= f[v][2]) flg = 1, f[u][2] += f[v][0];
else add = min(add, f[v][0] - f[v][2]), f[u][2] += f[v][2];
}
if (!flg) f[u][2] += add;
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
addEdge(u, v);
addEdge(v, u);
}
dfs(1, 0);
printf("%d\n", min(f[1][0], f[1][2]));
return 0;
}
CF1699C The Third Problem
思路:考虑从限制最多的 \(0\) 开始。\(0\) 显然只能原地不动,\(1\) 也是,\(2\) 如果在 \(0,1\) 之间则它在 \(0,1\) 之间随便放,否则也只能原地不动,以此类推。
算法:计数。
技巧:从限制多的开始考虑。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 1e5, P = int(1e9) + 7;
int n, a[N + 10], pos[N + 10];
void mian() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i), pos[a[i]] = i;
int l = pos[0], r = pos[0], ans = 1;
for (int i = 1; i < n; i++) {
if (pos[i] < l) l = pos[i];
else if (pos[i] > r) r = pos[i];
else ans = 1LL * ans * (r - l + 1 - i) % P;
}
printf("%d\n", ans);
}
int main() {
int T; scanf("%d", &T);
while (T--) {
for (int i = 0; i <= n; i++)
a[i] = pos[i] = 0;
mian();
}
return 0;
}
20220718
CF1093D Beautiful Graph
思路:如果图中的某个连通块不是二分图则无解。否则每个连通块的答案就是这个二分图两侧结点数的 \(\operatorname{exp2}\) 之和(一侧染 \(\{1,3\}\),另一侧全是 \(2\)),最终的答案就是每个连通块答案的积。
算法:二分图。
技巧:图上奇偶性问题考虑二分图。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <queue>
using namespace std;
const int N = 3e5, P = 998244353;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
int n, m, col[N + 10];
int siz1, siz2, vis[N + 10];
int pw[N + 10];
bool isBi(int s) {
queue<int> q;
q.push(s);
col[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (col[v] == 0) {
col[v] = 3 - col[u];
q.push(v);
} else if (col[v] == col[u]) return 0;
}
}
return 1;
}
void dfs(int u) {
if (vis[u]) return;
vis[u] = 1;
if (col[u] == 1) siz1++;
else siz2++;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
dfs(v);
}
}
void mian() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; scanf("%d%d", &u, &v);
addEdge(u, v), addEdge(v, u);
}
int ans = 1;
for (int i = 1; i <= n; i++)
if (!vis[i]) {
siz1 = siz2 = 0;
if (!isBi(i)) return puts("0"), void();
dfs(i);
ans = 1LL * ans * ((pw[siz1] + pw[siz2]) % P) % P;
}
printf("%d\n", ans);
}
int main() {
pw[0] = 1;
for (int i = 1; i <= N; i++)
pw[i] = (pw[i - 1] + pw[i - 1]) % P;
int T; scanf("%d", &T);
while (T--) {
tote = 0;
for (int i = 1; i <= n; i++)
vis[i] = col[i] = head[i] = 0;
mian();
}
return 0;
}
CF1081D Maximum Distance
思路:由 Kruskal 算法的性质,答案所在的边一定在 MST 上。并且把这条边从 MST 上割掉后两个连通块都有特殊点。最后输出 \(k\) 遍 \(ans\) 即可。
算法:MST。
技巧:Kruskal 算法的性质。
想到了的:无。
没想到的:都没想到。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 1e5;
struct Edge {
int u, v, w;
};
Edge e[N + 10];
int n, m, k, spec[N + 10];
int f[N + 10];
int find(int x) {
return f[x] == x ? x : f[x] = find(f[x]);
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= k; i++) {
int x; scanf("%d", &x);
spec[x] = 1;
}
for (int i = 1; i <= m; i++)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
sort(e + 1, e + m + 1, [](const Edge &lhs, const Edge &rhs) {
return lhs.w < rhs.w;
});
int ans = 0;
for (int i = 1; i <= n; i++) f[i] = i;
for (int i = 1; i <= m; i++) {
int u = e[i].u, v = e[i].v;
u = find(u), v = find(v);
if (u != v) {
f[u] = v ;
if (spec[u] && spec[v]) ans = e[i].w;
if (spec[u] || spec[v]) spec[u] = spec[v] = 1;
}
}
for (int i = 1; i <= k; i++)
printf("%d%c", ans, " \n"[i == k]);
return 0;
}
CF1585F Non-equal Neighbours
思路:首先有一个暴力的 dp:设 \(f_{i,j}\) 为考虑前 \(i\) 项且 \(b_i=j\) 时的答案,则:
这样是 \(\mathcal O(n^2)\) 的,于是继续优化之。首先把第一维压掉,然后用线段树优化。线段树需要支持的操作:区间 \(\times -1\)、区间加、区间赋值为 \(0\)(相当于 \(\times 0\))、全局查询和。
算法:数据结构优化 dp。
技巧:数据结构优化。
想到了的:朴素 dp,压维。
没想到的:线段树优化。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 2e5, P = 998244353;
struct Node {
int sum, atag, mtag;
Node() : sum(0), atag(0), mtag(1) {}
};
Node t[N * 4 + 10];
int n, a[N + 10], m, b[N + 10];
#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)
void pushUp(int i) {
t[i].sum = (t[ls(i)].sum + t[rs(i)].sum) % P;
}
void pushA(int i, int l, int r, int atag) {
(t[i].atag += atag) %= P;
(t[i].sum += 1LL * atag * (b[r] - b[l - 1]) % P) %= P;
}
void pushM(int i, int mtag) {
t[i].mtag = 1LL * t[i].mtag * mtag % P;
t[i].atag = 1LL * t[i].atag * mtag % P;
t[i].sum = 1LL * t[i].sum * mtag % P;
}
void pushDown(int i, int l, int r) {
if (t[i].atag != 0 || t[i].mtag != 1) {
int mid = (l + r) >> 1;
pushM(ls(i), t[i].mtag);
pushM(rs(i), t[i].mtag);
pushA(ls(i), l, mid, t[i].atag);
pushA(rs(i), mid + 1, r, t[i].atag);
t[i].atag = 0, t[i].mtag = 1;
}
}
void build(int i, int l, int r) {
if (l == r) return t[i].sum = b[l] - b[l - 1], void();
int mid = (l + r) >> 1;
build(ls(i), l, mid);
build(rs(i), mid + 1, r);
pushUp(i);
}
void modifyAdd(int i, int l, int r, int ql, int qr, int v) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) return pushA(i, l, r, v), void();
int mid = (l + r) >> 1;
pushDown(i, l, r);
modifyAdd(ls(i), l, mid, ql, qr, v);
modifyAdd(rs(i), mid + 1, r, ql, qr, v);
pushUp(i);
}
void modifyMul(int i, int l, int r, int ql, int qr, int v) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) return pushM(i, v), void();
int mid = (l + r) >> 1;
pushDown(i, l, r);
modifyMul(ls(i), l, mid, ql, qr, v);
modifyMul(rs(i), mid + 1, r, ql, qr, v);
pushUp(i);
}
#undef ls
#undef rs
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i), b[i] = a[i];
sort(b + 1, b + n + 1);
m = int(unique(b + 1, b + n + 1) - b - 1);
for (int i = 1; i <= n; i++)
a[i] = int(lower_bound(b + 1, b + m + 1, a[i]) - b);
build(1, 1, m);
modifyMul(1, 1, m, a[1] + 1, m, 0);
for (int i = 2; i <= n; i++) {
int sum = t[1].sum;
modifyMul(1, 1, m, 1, m, -1);
modifyAdd(1, 1, m, 1, m, sum);
modifyMul(1, 1, m, a[i] + 1, m, 0);
}
printf("%d\n", (t[1].sum % P + P) % P);
return 0;
}
20220719
CF1706E Qpwoeirut and Vertices
思路:由性质得答案一定在 MST 上。而树上 \(l\le a\le b\le r\) 的路径集合和 \(l\leftrightarrow l+1,l+1\leftrightarrow l+2,\cdots,r-1\leftrightarrow r\) 的并是一样的。于是我们可以用线段树维护 (\(i\leftrightarrow i+1\) 路径边权最大值) 的最大值。
算法:MST,线段树。
技巧:Kruskal 算法性质,树上路径并相关结论。
想到了的:考虑 MST。
没想到的:那个结论。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 1e5, M = 2e5, L = 20;
struct Edge {
int to, nxt, w;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v, int w) {
e[++tote] = {v, head[u], w};
head[u] = tote;
}
struct Edge2 {
int u, v, w;
Edge2() : u(0), v(0), w(0) {}
};
struct Node {
int mx, rval;
Node() : mx(0), rval(0) {}
};
int n, m, q;
Edge2 E[M + 10];
int f[N + 10];
int fa[N + 10][L + 5], dep[N + 10], mx[N + 10][L + 5], val[N + 10];
Node t[N * 4 + 10];
int find(int x) {
return f[x] == x ? x : f[x] = find(f[x]);
}
void kruskal() {
for (int i = 1; i <= n; i++) f[i] = i;
sort(E + 1, E + m + 1, [](const Edge2 &lhs, const Edge2 &rhs) {
return lhs.w < rhs.w;
});
for (int i = 1; i <= m; i++) {
int u = E[i].u, v = E[i].v;
u = find(u), v = find(v);
if (u != v) {
f[u] = v;
addEdge(E[i].u, E[i].v, E[i].w);
addEdge(E[i].v, E[i].u, E[i].w);
}
}
}
void dfs(int u, int ffa) {
fa[u][0] = ffa;
for (int i = 1; i <= L; i++) {
fa[u][i] = fa[fa[u][i - 1]][i - 1];
mx[u][i] = max(mx[u][i - 1], mx[fa[u][i - 1]][i - 1]);
}
dep[u] = dep[ffa] + 1;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == ffa) continue;
mx[v][0] = e[i].w;
dfs(v, u);
}
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int res = 0;
for (int i = L; i >= 0; i--)
if (dep[fa[u][i]] >= dep[v]) {
res = max(res, mx[u][i]);
u = fa[u][i];
}
if (u == v) return res;
for (int i = L; i >= 0; i--)
if (fa[u][i] != fa[v][i]) {
res = max({res, mx[u][i], mx[v][i]});
u = fa[u][i], v = fa[v][i];
}
return max({res, mx[u][0], mx[v][0]});
}
#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)
Node pushUp(const Node &L, const Node &R) {
Node res;
res.mx = max({L.mx, R.mx, L.rval});
res.rval = R.rval;
return res;
}
void build(int i, int l, int r) {
if (l == r) {
t[i].rval = val[l];
t[i].mx = 0;
return;
}
int mid = (l + r) >> 1;
build(ls(i), l, mid);
build(rs(i), mid + 1, r);
t[i] = pushUp(t[ls(i)], t[rs(i)]);
}
Node query(int i, int l, int r, int ql, int qr) {
if (qr < l || r < ql) return Node();
if (ql <= l && r <= qr) return t[i];
int mid = (l + r) >> 1;
Node L = query(ls(i), l, mid, ql, qr);
Node R = query(rs(i), mid + 1, r, ql, qr);
if (qr <= mid) return L;
if (ql > mid) return R;
return pushUp(L, R);
}
#undef ls
#undef rs
void mian() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= m; i++)
scanf("%d%d", &E[i].u, &E[i].v), E[i].w = i;
kruskal();
dfs(1, 0);
for (int i = 1; i < n; i++)
val[i] = lca(i, i + 1);
build(1, 1, n);
while (q--) {
int l, r; scanf("%d%d", &l, &r);
printf("%d ", query(1, 1, n, l, r).mx);
}
puts("");
}
int main() {
int T; scanf("%d", &T);
while (T--) {
tote = 0;
for (int i = 1; i <= n; i++) {
head[i] = dep[i] = val[i] = f[i] = 0;
for (int j = 0; j <= L; j++)
fa[i][j] = mx[i][j] = 0;
}
for (int i = 1; i <= m; i++)
E[i] = Edge2();
for (int i = 1; i <= n * 4; i++)
t[i] = Node();
mian();
}
return 0;
}
20220721
CF920E Connected Components?
思路:直接暴力,复杂度是对的。
算法:暴力。
技巧:暴力。
想到了的:无。
没想到的:都没想到。
代码
#include <algorithm>
#include <cstdio>
#include <queue>
#include <set>
using namespace std;
const int N = 2e5;
int n, m;
set<int> G[N + 10], st;
multiset<int> ans;
bool vis[N + 10];
int siz;
void bfs(int u) {
queue<int> q;
vis[u] = 1, st.erase(u), q.push(u);
while(!q.empty()) {
int u = q.front(); q.pop();
siz++;
set<int> _ = st;
for (auto v : _)
if (G[u].find(v) == G[u].end() && !vis[v])
vis[v] = 1, st.erase(v), q.push(v);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; scanf("%d%d", &u, &v);
G[u].insert(v), G[v].insert(u);
}
for (int i = 1; i <= n; i++) st.insert(i);
for (int i = 1; i <= n; i++)
if (!vis[i]) {
siz = 0;
bfs(i);
ans.insert(siz);
}
printf("%d\n", int(ans.size()));
for (auto x : ans) printf("%d ", x);
puts("");
return 0;
}
20220723
P3873 [TJOI2010]天气预报
思路:考虑构造转移矩阵:
算法:矩阵。
技巧:矩阵加速递推。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 100, P = 4147;
int n, m;
struct Matrix {
int a[N + 10][N + 10];
Matrix() { memset(a, 0, sizeof(a)); }
int *operator[](const int i) { return a[i]; }
const int *operator[](const int i) const { return a[i]; }
void unit(int n) {
for (int i = 1; i <= n; i++) a[i][i] = 1;
}
};
Matrix operator*(const Matrix &A, const Matrix &B) {
Matrix res;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= n; k++)
(res[i][j] += 1LL * A[i][k] * B[k][j] % P) %= P;
return res;
}
Matrix qpow(Matrix A, int b) {
Matrix res; res.unit(n);
while (b) {
if (b & 1) res = res * A;
A = A * A;
b >>= 1;
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
Matrix A, W;
for (int i = 1; i <= n; i++) scanf("%d", &W[1][i]);
for (int i = 1; i <= n; i++) scanf("%d", &A[i][1]);
for (int i = 1; i <= n; i++) A[i - 1][i] = 1;
printf("%d\n", (W * qpow(A, m - n))[1][1]);
return 0;
}
ABC261E Many Operations
思路:按位考虑。于是我们可以预处理 \(f_{i,j,k}\)——令第 \(j\) 位一开始为 \(k\)(\(k\in\{0,1\}\)),对 \(k\) 进行前 \(i\) 次操作,最终的结果是什么。
算法:位运算。
技巧:按位考虑。
想到了的:都想到了。
没想到的:无。
代码
const int N = 2e5, M = 30;
int n, x, opt[N + 10], a[N + 10];
int f[N + 10][M + 5][2];
void mian() {
scanf("%d%d", &n, &x);
for (int i = 1; i <= n; i++)
scanf("%d%d", opt + i, a + i);
for (int j = 0; j <= M; j++)
for (int k = 0; k < 2; k++) {
f[0][j][k] = k;
for (int i = 1; i <= n; i++) {
if (opt[i] == 1) f[i][j][k] = (f[i - 1][j][k] & ((a[i] >> j) & 1));
if (opt[i] == 2) f[i][j][k] = (f[i - 1][j][k] | ((a[i] >> j) & 1));
if (opt[i] == 3) f[i][j][k] = (f[i - 1][j][k] ^ ((a[i] >> j) & 1));
}
}
for (int i = 1; i <= n; i++) {
int y = 0;
for (int j = 0; j <= M; j++)
if (f[i][j][(x >> j) & 1]) y |= (1 << j);
printf("%d\n", y);
x = y;
}
}
ABC261F Sorting Color Balls
思路:答案即不考虑颜色时的逆序对数减去相同颜色逆序对数。
- 对于一个序列,每次交换相邻的两个数,使它升序的最小操作次数为这个序列的逆序对数!对于一个序列,每次交换相邻的两个数,使它升序的最小操作次数为这个序列的逆序对数!对于一个序列,每次交换相邻的两个数,使它升序的最小操作次数为这个序列的逆序对数!
- 正难则反!正难则反!正难则反!
算法:逆序对。
技巧:对于一个序列,每次交换相邻的两个数,使它升序的最小操作次数为这个序列的逆序对数;正难则反。
想到了的:都想到了。
没想到的:无。
代码
const int N = 3e5;
struct Node {
int c, x;
};
int n, m;
Node a[N + 10];
ll ans;
int c[N + 10];
int qwq[N + 10], tmp[N + 10];
vector<int> pos[N + 10];
#define lowbit(x) (x & (-x))
void modify(int p, int x) {
for (; p <= m; p += lowbit(p)) c[p] += x;
}
int query(int p) {
int res = 0;
for (; p; p -= lowbit(p)) res += c[p];
return res;
}
#undef lowbit
ll calc(int n, int *a) {
for (int i = 1; i <= m; i++) c[i] = 0;
for (int i = 1; i <= n; i++) tmp[i] = a[i];
sort(tmp + 1, tmp + n + 1);
m = int(unique(tmp + 1, tmp + n + 1) - tmp - 1);
for (int i = 1; i <= n; i++)
a[i] = int(lower_bound(tmp + 1, tmp + m + 1, a[i]) - tmp);
ll res = 0;
for (int i = 1; i <= n; i++) {
res += query(m) - query(a[i]);
modify(a[i], 1);
}
return res;
}
void mian() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i].c);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i].x), qwq[i] = a[i].x;
ans = calc(n, qwq);
for (int i = 1; i <= n; i++)
pos[a[i].c].push_back(a[i].x);
for (int i = 1; i <= n; i++) {
int tot = 0;
for (auto x : pos[i]) qwq[++tot] = x;
if (tot == 0) continue;
ans -= calc(int(pos[i].size()), qwq);
}
printf("%lld\n", ans);
}
20220724
P2458 [SDOI2006]保安站岗
同这个题。
P7689 [CEOI2002] Bugs Integrated,Inc.
见题解。
20220725
CF1527E Partition Game
思路:设 \(f_{i,j}\) 表示考虑前 \(i\) 项分成 \(j\) 段的答案。决策单调性分治即可。
算法:决策单调性分治优化 dp。
技巧:决策单调性分治。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 35000, K = 100;
int n, k, a[N + 10], pre[N + 10], suc[N + 10], pos[N + 10];
ll f[K + 5][N + 10], res;
int nowl, nowr;
ll calc(int l, int r) {
while (nowl > l) nowl--, res += suc[nowl] <= nowr ? suc[nowl] - nowl : 0;
while (nowr < r) nowr++, res += pre[nowr] >= nowl ? nowr - pre[nowr] : 0;
while (nowl < l) res -= suc[nowl] <= nowr ? suc[nowl] - nowl : 0, nowl++;
while (nowr > r) res -= pre[nowr] >= nowl ? nowr - pre[nowr] : 0, nowr--;
return res;
}
void solve(int j, int l, int r, int L, int R) {
if (l > r || L > R) return;
int mid = (l + r) >> 1, p = 0;
for (int i = L; i <= min(R, mid); i++) {
ll val = f[j - 1][i - 1] + calc(i, mid);
if (val <= f[j][mid]) f[j][mid] = val, p = i;
}
solve(j, l, mid - 1, L, p);
solve(j, mid + 1, r, p, R);
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
for (int i = 1; i <= n; i++) pre[i] = pos[a[i]], pos[a[i]] = i;
for (int i = 1; i <= n; i++) pos[a[i]] = n + 1;
for (int i = n; i >= 1; i--)
suc[i] = pos[a[i]], pos[a[i]] = i;
memset(f, 0x3f, sizeof(f));
f[0][0] = 0;
nowl = 1, nowr = 0;
for (int i = 1; i <= k; i++)
solve(i, 1, n, 1, n);
printf("%lld\n", f[k][n]);
return 0;
}
CF868F Yet Another Minimization Problem
思路:设 \(f_{i,j}\) 表示考虑前 \(i\) 项分成 \(j\) 段的答案。决策单调性分治即可。
算法:决策单调性分治优化 dp。
技巧:决策单调性分治。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 2e5, K = 20;
int n, k, a[N + 10];
ll f[K + 5][N + 10], res;
int buc[N + 10], nowl, nowr;
void add(int x) {
res += buc[a[x]];
buc[a[x]]++;
}
void del(int x) {
buc[a[x]]--;
res -= buc[a[x]];
}
ll calc(int l, int r) {
while (nowl > l) add(--nowl);
while (nowr < r) add(++nowr);
while (nowl < l) del(nowl++);
while (nowr > r) del(nowr--);
return res;
}
void solve(int j, int l, int r, int L, int R) {
if (l > r || L > R) return;
int mid = (l + r) >> 1, pos = 0;
for (int i = L; i <= min(R, mid); i++) {
ll val = f[j - 1][i - 1] + calc(i, mid);
if (val <= f[j][mid]) f[j][mid] = val, pos = i;
}
solve(j, l, mid - 1, L, pos);
solve(j, mid + 1, r, pos, R);
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
memset(f, 0x3f, sizeof(f));
f[0][0] = 0;
nowl = 1, nowr = 0;
for (int i = 1; i <= k; i++)
solve(i, 1, n, 1, n);
printf("%lld\n", f[k][n]);
return 0;
}
20220726
CF1707C DFS Trees
思路:copied from https://www.cnblogs.com/ruierqwq/p/CF-1707C.html。
因为所有边权两两不同,所以 MST 是唯一的,我们把 MST 上的边标记出来。
我们知道对图进行 DFS 后,只有树边和返祖边两类边。要使得 MST 上的边均为树边,则不在 MST 上的边只能为返祖边。也就是说,不在 MST 上的边在当前根下必须是祖先后代关系。
至此,原问题转化为:判断每个节点作为根时,所有非 MST 边的两个点是否都是祖先后代关系。
我们可以随便找一个点当根,把每条非 MST 边两侧子树每个点的值加一(只有在这些点当根时才是祖先后代关系),然后判断每个点的值是否等于 \(m-n+1\) 即可。具体实现方法可以使用树上差分。
算法:MST、DFS 树、树上差分。
技巧:无向图的 DFS 树上只有树边和返祖边、考虑每条边对答案的贡献。
想到了的:MST。
没想到的:无向图的 DFS 树上只有树边和返祖边。
代码
#include <algorithm>
#include <cstdio>
#include <string>
using namespace std;
const int N = 1e5, M = 2e5, L = 20;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
struct Edge2 {
int u, v, w;
};
int n, m;
Edge2 E[M + 10];
int f[N + 10], dep[N + 10], fa[N + 10][L + 5];
int cnt[N + 10];
bool inMST[M + 10];
int find(int x) { return x == f[x] ? x : f[x] = find(f[x]); }
void kruskal() {
sort(E + 1, E + m + 1, [](const Edge2 &lhs, const Edge2 &rhs) {
return lhs.w < rhs.w;
});
for (int i = 1; i <= n; i++) f[i] = i;
for (int i = 1; i <= m; i++) {
int u = E[i].u, v = E[i].v;
if (find(u) != find(v)) {
inMST[i] = 1;
f[find(u)] = find(v);
addEdge(u, v), addEdge(v, u);
}
}
}
void dfs1(int u, int ffa) {
fa[u][0] = ffa, dep[u] = dep[ffa] + 1;
for (int i = 1; i <= L; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == ffa) continue;
dfs1(v, u);
}
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (int i = L; i >= 0; i--)
if (dep[fa[u][i]] >= dep[v])
u = fa[u][i];
if (u == v) return u;
for (int i = L; i >= 0; i--)
if (fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
int jump(int u, int v) {
for (int i = L; i >= 0; i--)
if (dep[fa[u][i]] > dep[v])
u = fa[u][i];
return u;
}
void dfs2(int u, int ffa) {
cnt[u] += cnt[ffa];
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == ffa) continue;
dfs2(v, u);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
scanf("%d%d", &E[i].u, &E[i].v), E[i].w = i;
kruskal();
dfs1(1, 0);
for (int i = 1; i <= m; i++)
if (!inMST[i]) {
int u = E[i].u, v = E[i].v, l = lca(u, v);
if (dep[u] > dep[v]) swap(u, v);
if (u == l) cnt[1]++, cnt[v]++, cnt[jump(v, u)]--;
else cnt[u]++, cnt[v]++;
}
dfs2(1, 0);
for (int i = 1; i <= n; i++)
printf("%d", cnt[i] == m - n + 1);
puts("");
return 0;
}
CF1111E Tree
思路:
记询问中涉及到的结点为“关键点”。
首先对于每组询问,我们可以做一个 dp:设 \(f_{i,j}\) 表示考虑前 \(i\) 个关键点,分成 \(j\) 组时的方案数,则:
\(u\) 表示第 \(i\) 个关键点(转移顺序下文有提到),\(cnt_u\) 表示第 \(i\) 个关键点 \(u\) 到根的路径上有多少个关键点(包括它自己)。
这里为了保证 dp 没有后效性,必须保证父亲结点在孩子结点之前被计算,也就是说我们按照 \(cnt_u\) 从小到大的顺序转移即可。
\(f_{i-1,j-1}\) 就是让 \(u\) 另起炉灶,自成一组,\((j-cnt_u+1)f_{i-1,j}\) 表示将 \(u\) 放到原来的某个组里,因为 \(u\) 不能和它的祖先一个组,所以有 \(j-cnt_u+1\) 个组可供选择(\(+1\) 是因为 \(u\) 本身不算)。
至此本题已经解决了一半。我们现在的问题是:在根结点 \(r\) 可变的情况下,\(cnt\) 如何维护?
\(cnt_u\) 其实就是 \(u\) 到 \(r\) 上有多少个关键点,它是一个单点加链上和问题,这和 \(r\) 是什么没关系!于是我们可以始终将根当作 \(1\) 处理。
对于链上和问题,我们可以使用树上差分的思想处理。于是只需求 \(\operatorname{calc}(x)\)——\(1\) 到 \(x\) 的路径上有多少个关键点。这个怎么做呢?
考虑一个关键点 \(v\) 对 \(\operatorname{calc}\) 的贡献。对于每个 \(u\),如果 \(u\) 在 \(v\) 的子树内,则 \(v\) 对 \(\operatorname{calc}(u)\) 贡献了 \(1\)。至此,我们只需解决一个子树加单点查的题。
子树加单点查是一个经典问题。子树在树的 DFS 序上是一个连续段,于是我们只需在 DFS 序上区间加单点查,树状数组维护差分序列即可。
算法:dp、数据结构。
技巧:分组问题考虑类似第二类斯特林数的 dp。
想到了的:无。
没想到的:都没想到。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 1e5, M = 300, L = 20, P = int(1e9) + 7;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
int n, q, id[N + 10], cnt[N + 10], f[N + 10][M + 5];
int dfn[N + 10], siz[N + 10], dfncnt, dep[N + 10], fa[N + 10][L + 5];
int c[N + 10];
#define lowbit(x) (x & (-x))
void modify(int p, int x) {
for (; p <= n; p += lowbit(p)) c[p] += x;
}
int query(int p) {
int res = 0;
for (; p; p -= lowbit(p)) res += c[p];
return res;
}
#undef lowbit
void dfs(int u, int ffa) {
dfn[u] = ++dfncnt; siz[u] = 1;
fa[u][0] = ffa; dep[u] = dep[ffa] + 1;
for (int i = 1; i <= L; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == ffa) continue;
dfs(v, u);
siz[u] += siz[v];
}
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (int i = L; i >= 0; i--)
if (dep[fa[u][i]] >= dep[v])
u = fa[u][i];
if (u == v) return u;
for (int i = L; i >= 0; i--)
if (fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
int query(int u, int v) {
int l = lca(u, v);
return query(dfn[u]) + query(dfn[v]) - query(dfn[l]) - query(dfn[fa[l][0]]);
}
int main() {
scanf("%d%d", &n, &q);
for (int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
addEdge(u, v), addEdge(v, u);
}
dfs(1, 0);
while (q--) {
int k, m, r; scanf("%d%d%d", &k, &m, &r);
for (int i = 1; i <= k; i++) {
scanf("%d", id + i);
modify(dfn[id[i]], 1), modify(dfn[id[i]] + siz[id[i]], -1);
}
for (int i = 1; i <= k; i++)
cnt[id[i]] = query(id[i], r);
sort(id + 1, id + k + 1, [&](int lhs, int rhs) {
return cnt[lhs] < cnt[rhs];
});
f[0][0] = 1;
for (int i = 1; i <= k; i++)
for (int j = 1; j <= m; j++)
f[i][j] = (f[i - 1][j - 1] + 1LL * f[i - 1][j] * (j - cnt[id[i]] + 1) % P) % P;
int ans = 0;
for (int i = 1; i <= m; i++)
(ans += f[k][i]) %= P;
printf("%d\n", ans);
for (int i = 1; i <= k; i++)
modify(dfn[id[i]], -1), modify(dfn[id[i]] + siz[id[i]], 1);
}
return 0;
}
20220727
CF685B Kay and Snowflake
思路:树的重心的性质:树的重心在根结点到它重儿子子树重心的路径上。计算 \(u\) 的重心时暴力从 \(son_u\) 的重心往上跳即可。
算法:树的重心。
技巧:树的重心在根结点到它重儿子子树重心的路径上。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 3e5;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
int n, m, fa[N + 10], ans[N + 10], siz[N + 10], son[N + 10];
void dfs(int u, int ffa) {
siz[u] = 1;
bool flg = 0;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == ffa) continue;
flg = 1;
dfs(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
}
if (!flg) ans[u] = u;
else {
ans[u] = ans[son[u]];
while (ans[u] != u) {
if (siz[son[ans[u]]] <= siz[u] / 2 && siz[u] - siz[ans[u]] <= siz[u] / 2) break;
ans[u] = fa[ans[u]];
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 2; i <= n; i++) {
scanf("%d", fa + i);
addEdge(fa[i], i), addEdge(i, fa[i]);
}
dfs(1, 0);
while (m--) {
int x; scanf("%d", &x);
printf("%d\n", ans[x]);
}
return 0;
}
CF1406C Link Cut Centroids
思路:一棵树最多有两个重心。如果有一个那么随便做,如果有两个我们可以直接从一个重心的子树里找一个叶子接到另一个重心上。
算法:树的重心。
技巧:构造。
想到了的:都想到的。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <vector>
#include <tuple>
using namespace std;
const int N = 1e5;
struct Edge {
int to, nxt;
} e[N * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v) {
e[++tote] = {v, head[u]};
head[u] = tote;
}
int n, siz[N + 10];
vector<int> cent;
void dfs1(int u, int fa) {
siz[u] = 1;
int mx = 0;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == fa) continue;
dfs1(v, u);
siz[u] += siz[v];
mx = max(siz[v], mx);
}
mx = max(mx, n - siz[u]);
if (mx <= n / 2) cent.push_back(u);
}
pair<int, int> dfs2(int u, int fa) {
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == fa) continue;
return dfs2(v, u);
}
return {u, fa};
}
void mian() {
scanf("%d", &n);
int uu, vv;
for (int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
addEdge(v, u), addEdge(u, v);
uu = u, vv = v;
}
dfs1(1, 0);
if (int(cent.size()) == 1)
printf("%d %d\n%d %d\n", uu, vv, uu, vv);
else {
tie(uu, vv) = dfs2(cent[0], cent[1]);
printf("%d %d\n%d %d\n", uu, vv, uu, cent[1]);
}
}
int main() {
int T; scanf("%d", &T);
while (T--) {
for (int i = 1; i <= n; i++) head[i] = siz[i] = 0; tote = 0;
cent.clear();
mian();
}
return 0;
}
20220729
CF1152C Neko does Maths
思路:
于是我们就应该让 \(g=\gcd(a-b,a+k)\) 尽可能大,只需让 \(g=a-b\) 或者 \(g=a+k\) 即可。注意到 \(a-b\) 是定的,枚举 \(a-b\) 的因数即可。
算法:简单数论、贪心。
技巧:\(\gcd\) 性质。
想到的了:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long ll;
ll calc(ll a, ll b, ll k) {
return (a + k) / __gcd(a + k, b + k) * (b + k);
}
int main() {
int a, b;
scanf("%d%d", &a, &b);
if (a == b) puts("0");
else {
if (a < b) swap(a, b);
int d = a - b;
// gcd(a - b, a + k) = a - b
ll ans = 1LL * ((a - 1) / (a - b)) * (a - b) - b, mn = calc(a, b, ans);
for (int i = 1; i <= int(sqrt(d)); i++)
if (d % i == 0) {
// gcd(a - b, a + k) = a + k
ll now = d / i - b;
if (now >= 0 && calc(a, b, now) < mn) {
mn = calc(a, b, now);
ans = now;
} else if (calc(a, b, now) == mn) ans = min(ans, now);
now = i - b;
if (now >= 0 && calc(a, b, now) < mn) {
mn = calc(a, b, now);
ans = now;
} else if (calc(a, b, now) == mn) ans = min(ans, now);
}
printf("%lld\n", ans);
}
return 0;
}
20220805
CF1176D Recover it!
思路:贪心地从大到小匹配即可。
算法:简单数论、贪心。
技巧:按顺序考虑。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <functional>
#include <vector>
using namespace std;
const int N = 4e5, M = 3e6;
int n, a[N + 10], b[N + 10], prm[N + 10], totp, id[M + 10];
bool notPrm[M + 10], vis[N + 10];
vector<int> pos[M + 10];
void sieve() {
notPrm[1] = 1;
for (int i = 2; i <= M; i++) {
if (!notPrm[i]) prm[++totp] = i, id[i] = totp;
for (int j = 1; j <= totp && i * prm[j] <= M; j++) {
notPrm[i * prm[j]] = 1;
if (i % prm[j] == 0) break;
}
}
}
int main() {
sieve();
scanf("%d", &n);
for (int i = 1; i <= 2 * n; i++)
scanf("%d", b + i);
sort(b + 1, b + 2 * n + 1, greater<int>());
for (int i = 2 * n; i >= 1; i--)
pos[b[i]].push_back(i);
int tota = 0;
for (int i = 1; i <= 2 * n; i++) {
if (vis[i]) continue;
if (!notPrm[b[i]]) {
a[++tota] = id[b[i]];
vis[pos[b[i]].back()] = 1;
pos[b[i]].pop_back();
vis[pos[id[b[i]]].back()] = 1;
pos[id[b[i]]].pop_back();
} else {
for (int j = 1; j <= totp; j++)
if (b[i] % prm[j] == 0) {
a[++tota] = b[i];
vis[pos[b[i] / prm[j]].back()] = 1;
pos[b[i] / prm[j]].pop_back();
vis[pos[b[i]].back()] = 1;
pos[b[i]].pop_back();
break;
}
}
}
for (int i = 1; i <= n; i++)
printf("%d%c", a[i], " \n"[i == n]);
return 0;
}
20220810
P4301 [CQOI2013] 新Nim游戏
思路:先手要胜利,则必须让后手无论拿掉哪几堆都弄不出异或和为 \(0\) 的情况。考虑线性基,如果不能成功插入一个数那么就不能选他,否则就能选。那么我们按从大到小的顺序贪心即可。
算法:博弈论、线性基、贪心。
技巧:按顺序考虑、博弈论问题考虑线性基。
想到了的:线性基。
没想到的:贪心。
代码
#include <algorithm>
#include <cstdio>
#include <functional>
using namespace std;
typedef long long ll;
const int N = 100, K = 30;
int n, a[N + 10], p[K + 10];
ll ans;
bool insert(int x) {
for (int i = K; i >= 0; i--) {
if (!((x >> i) & 1)) continue;
if (!p[i]) return p[i] = x, 1;
x ^= p[i];
}
return 0;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
sort(a + 1, a + n + 1, greater<int>());
for (int i = 1; i <= n; i++)
if (!insert(a[i])) ans += a[i];
printf("%lld\n", ans);
return 0;
}
CF1100F Ivan and Burgers
思路:离线后使用时间戳线性基。
算法:时间戳线性基。
技巧:时间戳。
想到了的:(模板题)
没想到的:(模板题)
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 5e5, L = 20;
struct Query {
int l, r, id;
};
int n, m, a[N + 10], p[L + 10], stp[N + 10];
Query q[N + 10];
int ans[N + 10];
void insert(int x, int y) {
for (int i = L; i >= 0; i--) {
if (!((x >> i) & 1)) continue;
if (!p[i]) return p[i] = x, stp[i] = y, void();
if (y > stp[i]) swap(x, p[i]), swap(y, stp[i]);
x ^= p[i];
}
}
int query(int x) {
int res = 0;
for (int i = L; i >= 0; i--)
if ((res ^ p[i]) > res && stp[i] >= x)
res ^= p[i];
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
scanf("%d", &m);
for (int i = 1; i <= m; i++)
scanf("%d%d", &q[i].l, &q[i].r), q[i].id = i;
sort(q + 1, q + m + 1, [](const Query &lhs, const Query &rhs) {
return lhs.r < rhs.r;
});
for (int i = 1, j = 1; i <= m; i++) {
while (j <= q[i].r) {
insert(a[j], j);
j++;
}
ans[q[i].id] = query(q[i].l);
}
for (int i = 1; i <= m; i++)
printf("%d\n", ans[i]);
return 0;
}
20220811
P4151 [WC2011]最大XOR和路径
思路:由异或的性质,一条路径可以等价为一个 \(1\) 到 \(n\) 的简单路径再并上若干个环。把图中的环插进线性基里,再随便找一个 \(1\) 到 \(n\) 的简单路径进行查询即可。
算法:线性基。
技巧:异或性质。
想到了的:(例题)
没想到的:(例题)
代码
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 5e4, M = 1e5, L = 60;
struct Edge {
int to, nxt;
ll w;
} e[M * 2 + 10];
int head[N + 10], tote;
void addEdge(int u, int v, ll w) {
e[++tote] = {v, head[u], w};
head[u] = tote;
}
int n, m;
ll p[L + 10], dis[N + 10];
bool vis[N + 10];
void insert(ll x) {
for (int i = L; i >= 0; i--) {
if (!((x >> i) & 1)) continue;
if (!p[i]) return p[i] = x, void();
x ^= p[i];
}
}
ll query(ll x) {
ll res = x;
for (int i = L; i >= 0; i--)
res = max(res, res ^ p[i]);
return res;
}
void dfs(int u, ll now) {
dis[u] = now; vis[u] = 1;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (!vis[v]) dfs(v, now ^ e[i].w);
else insert(dis[u] ^ dis[v] ^ e[i].w);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; ll w; scanf("%d%d%lld", &u, &v, &w);
addEdge(u, v, w), addEdge(v, u, w);
}
dfs(1, 0);
printf("%lld\n", query(dis[n]));
return 0;
}
20220826
CF1718A Burenka and Traditions
思路:发现当 \(a_l\oplus a_{l+1}\oplus\cdots\oplus a_r\) 时,我们可以用 \(r-l\) 的代价把 \([l,r]\) 清零。于是我们设 \(f_i\) 表示把 \(a_1\sim a_i\) 清零的最小代价,dp 即可。
算法:位运算、dp。
技巧:找性质猜结论。
想到了的:无。
没想到的:都没想到。
代码
#include <algorithm>
#include <cstdio>
#include <map>
using namespace std;
const int N = 1e5;
int n, a[N + 10], pre[N + 10], f[N + 10];
void mian() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i), pre[i] = pre[i - 1] ^ a[i];
map<int, int> mp;
mp[0] = 0;
for (int i = 1; i <= n; i++) {
f[i] = f[i - 1] + 1;
if (mp.find(pre[i]) != mp.end())
f[i] = min(f[i], f[mp[pre[i]]] + i - mp[pre[i]] - 1);
mp[pre[i]] = i;
}
printf("%d\n", f[n]);
}
int main() {
int T; scanf("%d", &T);
while (T--) mian();
return 0;
}
20220827
P2659 美丽的序列
思路:枚举每个 \(a_i\) 作为最小值,从 \(i\) 开始尽可能向左向右扩展直到 \(\lt a_i\) 为止。可以用单调栈实现。
算法:单调栈。
技巧:单调栈。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 2e6;
int n, a[N + 10], stk[N + 10], L[N + 10], R[N + 10], top;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
for (int i = 1; i <= n; i++) {
while (top && a[stk[top]] >= a[i]) top--;
L[i] = stk[top];
stk[++top] = i;
}
top = 0;
for (int i = n; i >= 1; i--) {
while (top && a[stk[top]] >= a[i]) top--;
R[i] = top ? stk[top] : n + 1;
stk[++top] = i;
}
ll ans = 0;
for (int i = 1; i <= n; i++)
ans = max(ans, 1LL * a[i] * (R[i] - L[i] - 1));
printf("%lld\n", ans);
return 0;
}
CF1718B Fibonacci Strings
思路:显然我们要从大的 Fibonacci 数开始考虑,并且用尽量大的 \(c_i\) 去配它。这个可以用堆实现。
算法:贪心。
技巧:贪心地按从大到小(或从小到大)顺序考虑。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 100;
int n, a[N + 10], totf;
ll f[N + 10], sumf[N + 10];
void mian() {
scanf("%d", &n);
priority_queue<int> q;
ll sum = 0;
for (int i = 1; i <= n; i++)
scanf("%d", a + i), q.push(a[i]), sum += a[i];
int pos = -1;
for (int i = 1; i <= totf; i++)
if (sumf[i] == sum) { pos = i; break; }
if (pos == -1) return puts("NO"), void();
int lst = 0;
for (int i = pos; i >= 1; i--) {
if (q.empty()) return puts("NO"), void();
int x = q.top(); q.pop();
if (lst) q.push(lst);
if (x < f[i]) return puts("NO"), void();
lst = x - f[i];
}
puts(q.empty() ? "YES" : "NO");
}
int main() {
f[++totf] = 1, f[++totf] = 1;
while (f[totf] <= int(1e9)) {
totf++;
f[totf] = f[totf - 1] + f[totf - 2];
}
for (int i = 1; i <= totf; i++)
sumf[i] = sumf[i - 1] + f[i];
int T; scanf("%d", &T);
while (T--) mian();
return 0;
}
20220830
AT4168 [ARC100C] Or Plus Max
思路:记 \(a\subseteq b\) 表示 \(a\) 中所有值为 \(1\) 的二进制位在 \(b\) 中的值也是 \(1\)。
我们考虑求 \(\max_{i,j\subseteq k,i\ne j}\{a_i+a_j\}\),那么答案就是前缀 \(\max\)。为什么?
上述式子成立是由于或运算的性质。或运算是不减的,并且 \(i,j\subseteq k\) 是 \(i\lor j=k\) 的必要条件、\(i\lor j\le k\) 的充分条件。这使得所有满足 \(i,j\subseteq k,i\ne j\) 的 \((i,j)\) 组成的集合保留了原来那些满足 \(i\lor j=k,i\ne j\) 的 \((i,j)\) 的集合,又没有把 \(i\lor j>k\) 的 \((i,j)\) 放进来,因此答案的正确性不变。(from https://www.luogu.com.cn/blog/_post/347172)
那么剩下的问题可以用高维前缀和维护 \(a\) 的最大值、次大值实现。
算法:位运算、高维前缀和(SOSdp)。
技巧:位运算转化。
想到的了:高维前缀和(SOSdp)。
没想到的:位运算转化。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 5e5;
struct Node {
int mx1, mx2;
};
int n;
Node f[N + 10];
Node merge(const Node &A, const Node &B) {
int tmp[] = {A.mx1, A.mx2, B.mx1, B.mx2};
sort(tmp, tmp + 4);
return {tmp[3], tmp[2]};
}
int main() {
scanf("%d", &n);
for (int i = 0; i < (1 << n); i++)
scanf("%d", &f[i].mx1), f[i].mx2 = -0x3f3f3f3f;
for (int j = 0; j < n; j++)
for (int i = 0; i < (1 << n); i++)
if ((i >> j) & 1) f[i] = merge(f[i], f[i ^ (1 << j)]);
int ans = 0;
for (int i = 1; i < (1 << n); i++) {
ans = max(ans, f[i].mx1 + f[i].mx2);
printf("%d\n", ans);
}
return 0;
}
P7706 「Wdsr-2.7」文文的摄影布置
思路:显然用线段树做。考虑合并的时候怎么合并,\(a_i+a_k-\min\{b_j\}\) 中的 \((i,j,k)\) 要么都在左边、要么都在右边、要么 \(i\) 在左边 \(j,k\) 在右边、要么 \(i,j\) 在右边 \(k\) 在左边。那么我们就要维护 \(\max\{a_i\},\max\{a_i-b_j\},\max\{b_i-a_j\}\),后面两个用类似的办法维护即可。
算法:线段树。
技巧:考虑全在左边,全在右边,跨过了左右两边三种情况。
想到了的:都想到了。
没想到的:无。
代码
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 5e5;
struct Node {
int res, mxa, mnb, mxlr, mxrl;
};
Node t[N * 4 + 10];
int n, m, a[N + 10], b[N + 10];
#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)
Node pushUp(const Node &L, const Node &R) {
Node res;
res.res = max({L.res, R.res, L.mxlr + R.mxa, L.mxa + R.mxrl});
res.mxa = max(L.mxa, R.mxa);
res.mnb = min(L.mnb, R.mnb);
res.mxlr = max({L.mxlr, R.mxlr, L.mxa - R.mnb});
res.mxrl = max({L.mxrl, R.mxrl, R.mxa - L.mnb});
return res;
}
void build(int i, int l, int r) {
if (l == r) return t[i].mxa = a[l], t[i].mnb = b[l], t[i].res = t[i].mxlr = t[i].mxrl = -0x3f3f3f3f, void();
int mid = (l + r) >> 1;
build(ls(i), l, mid);
build(rs(i), mid + 1, r);
t[i] = pushUp(t[ls(i)], t[rs(i)]);
}
void modify(int i, int l, int r, int p, int aa, int bb) {
if (l == r) return t[i].mxa = aa, t[i].mnb = bb, void();
int mid = (l + r) >> 1;
if (p <= mid) modify(ls(i), l, mid, p, aa, bb);
else modify(rs(i), mid + 1, r, p, aa, bb);
t[i] = pushUp(t[ls(i)], t[rs(i)]);
}
Node query(int i, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return t[i];
int mid = (l + r) >> 1;
if (qr <= mid) return query(ls(i), l, mid, ql, qr);
if (ql > mid) return query(rs(i), mid + 1, r, ql, qr);
return pushUp(query(ls(i), l, mid, ql, qr), query(rs(i), mid + 1, r, ql, qr));
}
#undef ls
#undef rs
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
for (int i = 1; i <= n; i++)
scanf("%d", b + i);
build(1, 1, n);
while (m--) {
int opt, x, y;
scanf("%d%d%d", &opt, &x, &y);
if (opt == 1) {
modify(1, 1, n, x, y, b[x]);
a[x] = y;
}
if (opt == 2) {
modify(1, 1, n, x, a[x], y);
b[x] = y;
}
if (opt == 3)
printf("%d\n", query(1, 1, n, x, y).res);
}
return 0;
}

浙公网安备 33010602011771号