[CF1610H]Squid Game

题目

传送门 to CF

题意概要
给出一棵 n n n 个点的树。对于两个点 a , b a,b a,b,在 a , b a,b a,b 间最短路径上找一个点 v v v 使得 d i s ( v , x ) dis(v,x) dis(v,x) 最小,记这个 v v v G a , b ( x ) G_{a,b}(x) Ga,b(x)

现在你要设置 k k k 个关键点 x 1 , x 2 , … , x k x_1,x_2,\dots,x_k x1,x2,,xk 使得 ∃ p ,    G a i , b i ( x p ) ∉ { a i , b i }    ( 1 ⩽ i ⩽ m ) \exist p,\;G_{a_i,b_i}(x_p)\notin\{a_i,b_i\}\;(1\leqslant i\leqslant m) p,Gai,bi(xp)/{ ai,bi}(1im) 。请最小化 k k k

数据范围与提示
n , m ⩽ 3 × 1 0 5 n,m\leqslant 3\times 10^5 n,m3×105

思路

首先,贪心 地想,有没有一个点能够解决很多限制呢?

这个点很容易找到,就是 根节点。将根节点设置为关键点,会使得所有非 直链 的限制都立刻被满足。但是根节点一定要设置为关键点吗?

有一个神奇的想法:如果只保留直链时的最优解不能覆盖全体非直链,则根节点必选。可悲的是,该结论需要用做法来证明;不证明该结论,又无法引入做法。所以出题人是怎么想到的?我表示不解。

先假定上面的结论是正确的;我们只需要解决直链。这是一个经典的 贪心,即放置的关键点越浅越好。为什么呢?考虑每次找到直链中上端点(靠近根称为上)最深的一条。它必须有一个关键点;这个关键点在其上端点以下。而其他所有的直链的上端点都在它之上,所以无法覆盖别人一定是因为关键点落在了别人下端点之下(而不会是因为在上端点之上)。那么放置一个更靠上的关键点,只会覆盖更多的直链。

具体实现的时候,可以直接做 d f s \tt dfs dfs,相当于按照上端点的深度从小到大访问,逆序处理(先递归处理子树)就可以了;两个不同子树之间的直链恰好无法互相影响。可以用树状数组配合 d f s \rm dfs dfs 序,每次都暴力检查一条直链的要求是否已经被满足;如果要求未被满足,而这个点又是上端点的儿子(再往上就没机会了)就在此处放置关键点。

最后,上面的结论如何证明?我们覆盖直链时,总是选择最靠上的点作为关键点,如果这还是不能覆盖非直链(落到了它的端点的子树内),就一定不行。这是严谨的,不信就试试反证法:如果存在一个方案能够不使关键点数量变多,而且能把非直链完全覆盖,说明找到了比原方案更 “靠上” 的方案,这与原方案的 “最靠上性” 和最优性矛盾。

时间复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 。我试着找到 O ( n ) \mathcal O(n) O(n) 的做法,以失败告终。

代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
#include <cstdlib>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long llong;
inline int readint(){
    
    
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar())
		if(c == '-') f = -f;
	for(; isdigit(c); c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

const int MAXN = 300005;
struct Edge{
    
    
	int to, nxt;
	Edge(int _t,int _n):to(_t),nxt(_n){
    
    }
	Edge() = default;
};
Edge e[MAXN<<1];
int head[MAXN], cntEdge;
void addEdge(int a,int b){
    
    
	e[cntEdge] = Edge(b,head[a]);
	head[a] = cntEdge ++;
}
# define _go(i,x) for(int i=head[x]; ~i; i=e[i].nxt)

int st[MAXN], ed[MAXN], dfn;
void scan(int x){
    
    
	st[x] = ++ dfn;
	_go(i,x) scan(e[i].to);
	ed[x] = dfn;
}

namespace BIT{
    
    
	int c[MAXN];
	void modify(int id){
    
    
		for(int i=id; i<MAXN; i+=(i&-i)) ++ c[i];
	}
	int query(int id){
    
    
		int res = 0;
		for(int i=id; i; i&=(i-1)) res += c[i];
		return res;
	}
}

int a[MAXN], b[MAXN];
std::vector<int> v[MAXN];
bool cmp(const int &x,const int &y){
    
    
	return st[b[x]] < st[b[y]]; // sort by dfn
}
void dfs(int x){
    
    
	_go(i,x) dfs(e[i].to);
	sort(v[x].begin(),v[x].end(),cmp);
	const int len = int(v[x].size());
	for(int i=head[x],j=0; ~i; i=e[i].nxt)
		for(; j!=len&&st[b[v[x][j]]]<=ed[e[i].to]; ++j){
    
    
			if(b[v[x][j]] == e[i].to)
				puts("-1"), exit(0);
			int c = BIT::query(ed[e[i].to]);
			c -= BIT::query(ed[b[v[x][j]]]);
			c += BIT::query(st[b[v[x][j]]]-1);
			c -= BIT::query(st[e[i].to]-1);
			if(c == 0) BIT::modify(st[e[i].to]);
		}
}

int main(){
    
    
	int n = readint(), m = readint();
	memset(head+1,-1,n<<2);
	rep(i,2,n) addEdge(readint(),i);
	scan(1); rep(i,1,m){
    
    
		a[i] = readint(), b[i] = readint();
		if(st[a[i]] > st[b[i]]) swap(a[i],b[i]);
		if(ed[a[i]] >= ed[b[i]]) v[a[i]].push_back(i);
	}
	dfs(1); int ans = BIT::query(n);
	rep(i,1,m) if(ed[a[i]] < ed[b[i]]){
    
    
		int c = ans-BIT::query(ed[b[i]]);
		if(!c) c += BIT::query(st[b[i]]-1)
			-BIT::query(ed[a[i]]); // (ed,st)
		if(!c) c += BIT::query(st[a[i]]-1);
		if(c == 0){
    
     ++ ans; break; }
	}
	printf("%d\n",ans);
	return 0;
}

后记

我既没有想到那个 “先有结论还是先有做法”,也没想到 “延迟贪心”。

毕竟我是废物嘛。我还以为是命运这种做法呢。

猜你喜欢

转载自blog.csdn.net/qq_42101694/article/details/122502337