对于一类依赖偏序关系计算答案的问题,由于我们只关注元素之间的大小关系,从而可以通过特殊的枚举方式来避免多种情况分类讨论。常见方法有:
-
\(\mathrm{<, >}\) :通过从小到大的方式依次考虑元素。
-
\(\mathrm{abs, max, min}\):通过拆成 \(<\) 或 \(>\) 的形式后,再从小到大考虑。
一些例题:
[模拟赛]dist
Statement:
给定一棵 \(n(n \le 10^6)\) 个节点带边权的树,定义 \(\mathrm{Min}(x, y)\) 是 \((x, y)\) 路径上的边权最小值。求 \(\max_{r = 1}^n {\sum_{v \ne i} \mathrm{Min}(r, v)}\)。
Solution:
我们只关注路径上最小的一条边,于是按边权从小到大依次考虑边带来的贡献,然后分成两个连通块做。但这样太慢了,所以逆向的进行合并,用并查集维护即可。
qwq
#include<bits/stdc++.h>
#pragma GCC optimize(3, "Ofast", "inline")
#define int long long
using namespace std;
const int N = 1e6 + 10;
int n, fa[N], siz[N], val[N];
struct Edge{
int u, v, w;
}E[N];
bool cmp(struct Edge E1, struct Edge E2){
return E1.w > E2.w;
}
int findfa(int x){return fa[x] = (fa[x] == x) ? x : findfa(fa[x]);}
void merge(int x, int y, int w){
int fx = findfa(x), fy = findfa(y);
if(fx == fy) return; if(siz[fx] < siz[fy]) swap(fx, fy);
val[fx] = max(val[fx] + siz[fy] * w, val[fy] + siz[fx] * w);
fa[fy] = fx; siz[fx] += siz[fy];
}
signed main(){
// freopen("dist3.in", "r", stdin);
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n; for(int i = 1; i <= n; i++) siz[i] = 1, fa[i] = i;
for(int i = 1; i < n; i++) cin >> E[i].u >> E[i].v >> E[i].w;
sort(E + 1, E + n, cmp);
for(int i = 1; i < n; i++) merge(E[i].u, E[i].v, E[i].w);
cout << val[findfa(1)];
return 0;
}
[模拟赛]博弈游戏
Statement:
\(\rm Alice\) 和 \(\rm Bob\) 正在一个有向图上玩游戏。初始图上的某个节点上放着棋子,他们轮流进行操作,每次选择棋子所在节点的一条出边把棋子移过去,如果没有出边则游戏直接结束。\(\rm Alice\) 先手,游戏在 \(10^{100}\) 步后结束。节点的分数恰好是它的编号(从 \(1\) 开始编号),\(\rm Alice\) 的最终得分是棋子所经过的节点的分数的最大值。\(\rm Alice\) 想最大化她的得分,\(\rm Bob\) 想最小化 \(\rm Alice\) 的得分。请问最优策略下,从每个节点出发,\(\rm Alice\) 的得分是多少?
Solution:
令 \(f_{u, 1/0}\) 是 \(\rm Alice\)/\(\rm Bob\) 执棋时的得分。显然有:
\[\begin{aligned} f_{u, 0} &= \max(u, \max(f_{v, 1} | \exists (u, v))) \\ f_{u, 1} &= \max(u, \min(f_{v, 0} | \exists (u, v))) \end{aligned} \]注意到 \(\max, \min\) 这类的偏序关系,于是考虑从大到小考虑加入每个点,并逐步确定每个状态的答案。假设现在枚举到点 \(u\),假设 \(f_{u, 0 / 1}\) 之前没有被确定下来,那么显然 \(f_{u, 0/1} = u\)。那么我们将新确定的答案加入到一个更行队列中,假设现在的状态是 \((u, id)\):
-
\(id = 0\):显然我们更新的是一些点的 \(f_{v, 1}\),那么注意到我们在从大到小枚举的过程中,最后一次更新到 \(f_{v, 1}\) 时才会对 \(f_{v, 1}\) 产生贡献,于是我们动态更行他的入度,当 \(v\) 的入度变为 \(0\) 时,就将 \(f_{v, 1}\) 赋值为 \(f_{u, 0}\)。
-
\(id = 1\):此时更新的是一些点的 \(f_{v, 0}\),只要 \(f_{v, 0}\) 没有被更新过,那么 \(f_{u, 1}\) 就是他的所有后继中最小的那个了。
拿队列更新就可以了。、
qwq
#include<bits/stdc++.h>
#define pir pair<int, int>
#pragma GCC optimize(3, "Ofast", "inline")
using namespace std;
const int N = 1e5 + 10;
int n, m, out[N], f[N][2];
struct edges{
int v, next;
}edges[N << 1];
int head[N], idx;
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
void solve(int rt){
queue<pir> Q;
if(!f[rt][0]) Q.push(make_pair(rt, 0)), f[rt][0] = rt;
if(!f[rt][1]) Q.push(make_pair(rt, 1)), f[rt][1] = rt;
while(!Q.empty()){
int u = Q.front().first, id = Q.front().second; Q.pop();
// cout << u << " " << id << " " << f[u][id] << "\n";
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v;
if(id == 0 && (!f[v][1])) f[v][1] = rt, Q.push(make_pair(v, 1));
else if(id == 1){
out[v]--;
if(!out[v]) f[v][0] = rt, Q.push(make_pair(v, 0));
}
}
}
}
signed main(){
// freopen("game.in", "r", stdin);
// freopen("game.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y; cin >> x >> y;
add_edge(y, x); out[x]++;
}
for(int i = n; i > 0; i--) solve(i);
for(int i = 1; i <= n; i++) cout << f[i][1] << " ";
return 0;
}