关于一类偏序问题

对于一类依赖偏序关系计算答案的问题,由于我们只关注元素之间的大小关系,从而可以通过特殊的枚举方式来避免多种情况分类讨论。常见方法有:

  • \(\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;
}
posted @ 2024-09-18 13:43  Little_corn  阅读(7)  评论(0编辑  收藏  举报