逐月信息学 2024 提高组 #3
\(\color{black}\texttt{A. 反转Dag图}\)
题目描述
给定一个有向图,每次操作可以花费 \(w_i\) 的代价来反转边 \(i\),最终总代价为每次操作代价的最大值。求最少需要多少代价才能使这张图变为一个 DAG。
思路
首先看这个问题的简化版:把反转操作变为删除操作。
可以用二分解决:二分出最终答案 \(x\),把所有代价 \(\le x\) 的边删掉,再拓扑排序判断即可。
这时可以发现,两个问题竟然是等价的:假设有 \(u\rightarrow v\) 这条边,如果 \(u\) 的拓扑序小于 \(v\),则这条边不用删除或翻转;否则这条边必须删除或翻转。即要删除一定会翻转,反之亦然。
代码
#include<bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int MAXN = 100001, MAXM = 100005;
struct Edge {
int v, w;
};
int n, m, in[MAXN];
vector<Edge> e[MAXN];
void FileIO(const string &s) {
freopen((s + ".in").c_str(), "r", stdin);
freopen((s + ".out").c_str(), "w", stdout);
}
bool check(int x) {
queue<int> que;
fill(in + 1, in + n + 1, 0);
for(int i = 1; i <= n; ++i) {
for(auto [v, w] : e[i]) {
in[v] += (w > x);
}
}
for(int i = 1; i <= n; ++i) {
if(!in[i]) {
que.push(i);
}
}
for(; !que.empty(); ) {
int u = que.front();
que.pop();
for(auto [v, w] : e[u]) {
if(w > x && !(--in[v])) {
que.push(v);
}
}
}
for(int i = 1; i <= n; ++i) {
if(in[i]) {
return 0;
}
}
return 1;
}
int Binary_Search() {
int l = 0, r = 1000000001;
for(; l < r; ) {
int mid = (l + r) >> 1;
(check(mid) ? r = mid : l = mid + 1);
}
return l;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
FileIO("antidag");
cin >> n >> m;
for(int i = 1, u, v, w; i <= m; ++i) {
cin >> u >> v >> w;
e[u].push_back({v, w});
}
cout << Binary_Search();
return 0;
}
\(\color{black}\texttt{B. 歪脖子树}\)
题目描述
给定一个根初始为 \(1\) 的树,每个点都有一个权值 \(A_i\),有 \(Q\) 次操作。每次操作为以下的一个:
- 将点 \(x\) 的权值改为 \(y\)。
- 将根重新定义为 \(x\)。
- 查询 \(x\) 子树内权值最小值。
对每个三操作给出答案。
思路
分类讨论+树链剖分。
对于每个三操作:
- 如果此时根在 \(x\) 的父亲子树内,则答案不变。
- 如果根就是 \(x\),则答案为整棵树的最小值。
- 如果根在 \(x\) 的某个儿子的子树内,则答案为除那个儿子子树外所有点最小值。
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100001;
struct Segment_Tree {
int l[4 * MAXN], r[4 * MAXN], Min[4 * MAXN], dfn[MAXN], a[MAXN];
void build(int u, int s, int t) {
l[u] = s, r[u] = t;
if(s == t) {
Min[u] = a[dfn[s]];
return;
}
int mid = (s + t) >> 1;
build(2 * u, s, mid), build(2 * u + 1, mid + 1, t);
Min[u] = min(Min[2 * u], Min[2 * u + 1]);
}
void update(int u, int p, int x) {
if(l[u] == r[u]) {
Min[u] = x;
return;
}
(p <= r[2 * u] ? update(2 * u, p, x) : update(2 * u + 1, p, x));
Min[u] = min(Min[2 * u], Min[2 * u + 1]);
}
int Get(int u, int s, int t) {
if(s > t) {
return INT_MAX;
}
if(l[u] >= s && r[u] <= t) {
return Min[u];
}
int x = INT_MAX;
if(s <= r[2 * u]) {
x = min(x, Get(2 * u, s, t));
}
if(t >= l[2 * u + 1]) {
x = min(x, Get(2 * u + 1, s, t));
}
return x;
}
}tr;
int n, q, f[MAXN][18], a[MAXN], ROOT = 1, dfn[MAXN], sz[MAXN], dep[MAXN], tot;
vector<int> e[MAXN];
void FileIO(const string &s) {
freopen((s + ".in").c_str(), "r", stdin);
freopen((s + ".out").c_str(), "w", stdout);
}
void dfs(int u, int fa) {
dfn[u] = ++tot, tr.dfn[tot] = u, sz[u] = 1, dep[u] = dep[fa] + 1;
for(int i = 1; i <= 17; ++i) {
f[u][i] = f[f[u][i - 1]][i - 1];
}
for(int v : e[u]) {
if(v != fa) {
dfs(v, u);
sz[u] += sz[v];
}
}
}
int Get(int u, int v) {
int d = dep[v] - dep[u] - 1;
for(int i = 17; i >= 0; --i) {
if((d >> i) & 1) {
v = f[v][i];
}
}
return v;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
FileIO("tree");
cin >> n >> q;
for(int i = 1; i <= n; ++i) {
cin >> f[i][0] >> tr.a[i];
if(f[i][0]) {
e[f[i][0]].push_back(i), e[i].push_back(f[i][0]);
}
}
dfs(1, 0);
tr.build(1, 1, n);
for(int i = 1, x, y; i <= q; ++i) {
char c;
cin >> c >> x;
if(c == 'V') {
cin >> y;
tr.update(1, dfn[x], y);
}else if(c == 'E') {
ROOT = x;
}else {
if(ROOT == x) {
cout << tr.Get(1, 1, n) << "\n";
}else if(dfn[ROOT] < dfn[x] || dfn[ROOT] >= dfn[x] + sz[x]) {
cout << tr.Get(1, dfn[x], dfn[x] + sz[x] - 1) << "\n";
}else {
int u = Get(x, ROOT);
cout << min(tr.Get(1, 1, dfn[u] - 1), tr.Get(1, dfn[u] + sz[u], n)) << "\n";
}
}
}
return 0;
}
\(\color{black}\texttt{C. 倒水问题}\)
题目描述
有 \(N\) 个水杯,每个水杯有 \(a_i\) 升水,令当前最大水量为 \(G\),你可以将所有水量 \(>\lfloor\frac{G}{2}\rfloor\) 的水杯倒空。一开始你可以事先把至多 \(K\) 杯水倒空。问最少需要多少次操作才能把所有水杯倒空。在此基础上找到事先倒的最少次数。
思路
首先将 \(a\) 排序,使用双指针求出每一杯水 \(i\) 最大的 \(l_i\) 使 \(a_{l_i} \le \lfloor \frac{a_i}{2}\rfloor\)。
由于总次数 \(\le \log \max \{a_i\}\),所以定义 \(dp_{i,j}\) 表示把前 \(i\) 杯水倒完,使用 \(j\) 次操作一开始最少要倒掉多少杯水,\(dp_{i,j}=\min (dp_{i-1,j}+1,dp_{l_i,j-1})\)。
时空复杂度均为 \(O(N\log \max \{a_i\})\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 200001, INF = int(1e9) + 1;
int n, k, a[MAXN], dp[MAXN][41], l[MAXN];
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
sort(a + 1, a + n + 1);
for(int i = n, j = n; i >= 1; --i) {
for(; j >= 1 && a[j] > a[i] / 2; --j) {
}
l[i] = j;
}
for(int i = 1; i <= 40; ++i) {
dp[0][i] = INF;
}
dp[0][0] = 0;
for(int i = 1; i <= n; ++i) {
for(int j = 0; j <= 40; ++j) {
dp[i][j] = min(dp[i - 1][j] + 1, (j ? dp[l[i]][j - 1] : INF));
}
}
for(int i = 0; i <= 40; ++i) {
if(dp[n][i] <= k) {
cout << i << " " << dp[n][i];
break;
}
}
return 0;
}
\(\color{black}\texttt{D. 树的颜色}\)
题目描述
给定一颗树,每条边初始为白色。每次有这些操作:
- 将 \(u\) 到 \(v\) 路径上所有边反色(即黑变白白变黑)。
- 将恰好有一个端点在 \(u\) 到 \(v\) 路径上的边反色。
- 查询 \(u\) 到 \(v\) 路径上黑色边的数量。
思路
对于 \(1\) 操作,直接树链剖分即可。
对于 \(2\) 操作,对于重边树剖,轻边打个标记。
对于 \(3\) 操作,对于重边,直接求和,对于轻边,判断两边是否打标记。
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100001;
struct Segment_Tree {
int l[4 * MAXN], r[4 * MAXN], sum[4 * MAXN];
bool lazy[4 * MAXN];
void build(int u, int s, int t) {
l[u] = s, r[u] = t, lazy[u] = sum[u] = 0;
if(s == t) {
return;
}
int mid = (s + t) >> 1;
build(2 * u, s, mid), build(2 * u + 1, mid + 1, t);
}
void tag(int u, bool x) {
sum[u] = (x ? r[u] - l[u] + 1 - sum[u] : sum[u]), lazy[u] ^= x;
}
void pushdown(int u) {
tag(2 * u, lazy[u]), tag(2 * u + 1, lazy[u]), lazy[u] = 0;
}
void update(int u, int s, int t) {
if(s > t) {
return;
}
if(l[u] >= s && r[u] <= t) {
tag(u, 1);
return;
}
pushdown(u);
if(s <= r[2 * u]) {
update(2 * u, s, t);
}
if(t >= l[2 * u + 1]) {
update(2 * u + 1, s, t);
}
sum[u] = sum[2 * u] + sum[2 * u + 1];
}
int Get(int u, int s, int t) {
if(s > t) {
return 0;
}
if(l[u] >= s && r[u] <= t) {
return sum[u];
}
pushdown(u);
int x = 0;
if(s <= r[2 * u]) {
x += Get(2 * u, s, t);
}
if(t >= l[2 * u + 1]) {
x += Get(2 * u + 1, s, t);
}
return x;
}
}tr, tr2;
int n, m, sz[MAXN], f[MAXN], son[MAXN], dfn[MAXN], _dfn[MAXN], top[MAXN], tot;
vector<int> e[MAXN];
void dfs(int u, int fa) {
sz[u] = 1, f[u] = fa;
for(int v : e[u]) {
if(v != fa) {
dfs(v, u);
sz[u] += sz[v];
if(sz[v] > sz[son[u]]) {
son[u] = v;
}
}
}
}
void DFS(int u, int fa) {
dfn[u] = ++tot, _dfn[tot] = u;
if(son[u]) {
top[son[u]] = top[u], DFS(son[u], u);
}
for(int v : e[u]) {
if(v != fa && v != son[u]) {
top[v] = v, DFS(v, u);
}
}
}
void Xorpath(int u, int v) {
for(; top[u] != top[v]; ) {
if(dfn[u] > dfn[v]) {
tr.update(1, dfn[top[u]], dfn[u]);
u = f[top[u]];
}else {
tr.update(1, dfn[top[v]], dfn[v]);
v = f[top[v]];
}
}
tr.update(1, min(dfn[u], dfn[v]) + 1, max(dfn[u], dfn[v]));
}
void Xor(int u, int v) {
for(; top[u] != top[v]; ) {
if(dfn[u] > dfn[v]) {
if(son[u]) {
tr.update(1, dfn[son[u]], dfn[son[u]]);
}
tr.update(1, dfn[top[u]], dfn[top[u]]);
tr2.update(1, dfn[top[u]], dfn[u]);
u = f[top[u]];
}else {
if(son[v]) {
tr.update(1, dfn[son[v]], dfn[son[v]]);
}
tr.update(1, dfn[top[v]], dfn[top[v]]);
tr2.update(1, dfn[top[v]], dfn[v]);
v = f[top[v]];
}
}
tr.update(1, min(dfn[u], dfn[v]), min(dfn[u], dfn[v]));
tr2.update(1, min(dfn[u], dfn[v]), max(dfn[u], dfn[v]));
if(son[(dfn[u] > dfn[v] ? u : v)]) {
tr.update(1, max(dfn[u], dfn[v]) + 1, max(dfn[u], dfn[v]) + 1);
}
}
int pathsum(int u, int v) {
int ans = 0;
for(; top[u] != top[v]; ) {
if(dfn[u] > dfn[v]) {
ans += tr.Get(1, dfn[top[u]] + 1, dfn[u]) + (tr2.Get(1, dfn[f[top[u]]], dfn[f[top[u]]]) ^ tr.Get(1, dfn[top[u]], dfn[top[u]]));
u = f[top[u]];
}else {
ans += tr.Get(1, dfn[top[v]] + 1, dfn[v]) + (tr2.Get(1, dfn[f[top[v]]], dfn[f[top[v]]]) ^ tr.Get(1, dfn[top[v]], dfn[top[v]]));
v = f[top[v]];
}
}
return ans + tr.Get(1, min(dfn[u], dfn[v]) + 1, dfn[u]) + tr.Get(1, min(dfn[u], dfn[v]) + 1, dfn[v]);
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1, u, v; i < n; ++i) {
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1, 0);
top[1] = 1;
DFS(1, 0);
cin >> m;
tr.build(1, 1, n), tr2.build(1, 1, n);
for(int i = 1, op, u, v; i <= m; ++i) {
cin >> op >> u >> v;
if(op == 1) {
Xorpath(u, v);
}else if(op == 2) {
Xor(u, v);
}else {
cout << pathsum(u, v) << "\n";
}
}
return 0;
}