题目
题意概要
给出一棵 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}(1⩽i⩽m) 。请最小化 k k k 。
数据范围与提示
n , m ⩽ 3 × 1 0 5 n,m\leqslant 3\times 10^5 n,m⩽3×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;
}
后记
我既没有想到那个 “先有结论还是先有做法”,也没想到 “延迟贪心”。
毕竟我是废物嘛。我还以为是命运这种做法呢。