更好的阅读体验请跳转至我的博客
Static Query on Tree
题意
给定一棵根节点是1,且只能向上走的树,每次询问给定三个集合 A , B , C A,B,C A,B,C,求有多少点可以从 A A A中某点到达,并也可以从 B B B中某点到达,并且该点可以到达 C C C中的某个节点。
思路
题解给了两种方法,其中树剖最好理解,第二种方案最巧妙。
方案一
树剖就每次将 A , B A,B A,B集合中的点到根节点打上标记, C C C集合点的子树打上标记,询问有多少点同时有三种标记即可,需要注意的是线段树维护的方法,我使用的是状态压缩成8种更新。
vector<int> G[N];
int dep[N],n,m,dfn[N],id,sz[N];
struct node {
ll son, dep, top, dfn, fa, sz, val;
//重儿子 节点深度 链顶编号 dfs序 父亲节点 子树大小 权值
}a[N];
#define son(x) (a[(x)].son)
#define dep(x) (a[(x)].dep)
#define top(x) (a[(x)].top)
#define dfn(x) (a[(x)].dfn)
#define fa(x) (a[(x)].fa)
#define sz(x) (a[(x)].sz)
#define val(x) (a[(x)].val)
void dfs(int x, int fa) {
dep(x) = dep(fa) + 1;
fa(x) = fa; sz(x) = 1;
for (int it : G[x]) {
if (it == fa) continue;
dfs(it, x);
if (sz(son(x)) < sz(it)) son(x) = it;
sz(x) += sz(it);
}
}
void dfs(int x, int fa, int top) {
dfn(x) = ++id;
top(x) = top;
if (!son(x)) return;
dfs(son(x), x, top);
for (int it : G[x]) {
if (it == son(x) || it == fa) continue;
dfs(it, x, it);
}
}
int tr[N][8],lazy[N][3];
void pushup(int now){
for(int i=0;i<(1<<3);i++){
tr[now][i]=tr[ls][i]+tr[rs][i];
}
}
void pushdown(int now){
for(int d=0;d<3;d++){
if(lazy[now][d]){
for(int i=0;i<(1<<3);i++){
if(i&(1<<d)){
tr[ls][i]+=tr[ls][i^(1<<d)];
tr[rs][i]+=tr[rs][i^(1<<d)];
tr[ls][i^(1<<d)]=tr[rs][i^(1<<d)]=0;
}
}
lazy[now][d]=0;
lazy[ls][d]=1;lazy[rs][d]=1;
}
}
}
void build(int now,int l,int r){
if(l==r){
tr[now][0]=1;
return ;
}
int mid=l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
tr[now][0]=tr[ls][0]+tr[rs][0];
}
void update(int now,int l,int r,int L,int R,int d){
if(L<=l&&r<=R){
for(int i=0;i<(1<<3);i++){
if(i&(1<<d)){
tr[now][i]+=tr[now][i^(1<<d)];
tr[now][i^(1<<d)]=0;
}
}
lazy[now][d]=1;
return ;
}
pushdown(now);
int mid=l+r>>1;
if(L<=mid) update(ls,l,mid,L,R,d);
if(R>mid) update(rs,mid+1,r,L,R,d);
pushup(now);
}
int query(int now,int l,int r,int L,int R){
if(L<=l&&r<=R) return tr[now][7];
pushdown(now);
int mid=l+r>>1;
int res=0;
if(L<=mid) res+=query(ls,l,mid,L,R);
if(R>mid) res+=query(rs,mid+1,r,L,R);
return res;
}
void modify_chain(int x,int y,int d){
while(top(x)!=top(y)){
if(dep(top(x))<dep(top(y))) swap(x,y);
update(1,1,n,dfn(top(x)),dfn(x),d);
x=fa(top(x));
}
if(dep(x)>dep(y)) swap(x,y);
update(1,1,n,dfn(x),dfn(y),d);
}
void cle(int now,int l,int r){//清空使用过的节点
if(tr[now][0]==r-l+1){
memset(lazy[now],0,sizeof lazy[now]);
return ;
}
memset(lazy[now],0,sizeof lazy[now]);
memset(tr[now],0,sizeof tr[now]);
tr[now][0]=r-l+1;
if(l==r) return ;
int mid=l+r>>1;
cle(ls,l,mid);
cle(rs,mid+1,r);
}
int main(){
IOS;
int T;
cin>>T;
while(T--){
id=0;
cin>>n>>m;
for(int i=1;i<=n;i++) G[i].clear();
for(int i=1;i<n;i++){
int x;
cin>>x;
G[x].push_back(i+1);
}
dfs(1,0);
dfs(1,0,1);
build(1,1,n);
while(m--){
int k1,k2,k3;
cin>>k1>>k2>>k3;
vector<int> A,B,C;
for(int i=1;i<=k1;i++){
int x;
cin>>x;
modify_chain(1,x,0);
}
for(int i=1;i<=k2;i++){
int x;
cin>>x;
modify_chain(1,x,1);
}
for(int i=1;i<=k3;i++){
int x;
cin>>x;
update(1,1,n,dfn(x),dfn(x)+sz(x)-1,2);
}
cout<<tr[1][7]<<endl;
cle(1,1,n);//clear
}
}
return 0;
}
后部分可以不使用线段树,而是判断 A , B , C A,B,C A,B,C集合中线段的重复区间的长度,即线段的交集。可以使用类似扫描线的思想,由于只有三种,复杂度为 O ( n ) O(n) O(n),树剖 O ( n l o g ) O(nlog) O(nlog)。
vector<int> G[N];
int dep[N],n,m,dfn[N],id,sz[N];
struct node {
ll son, dep, top, dfn, fa, sz, val;
//重儿子 节点深度 链顶编号 dfs序 父亲节点 子树大小 权值
}a[N];
#define son(x) (a[(x)].son)
#define dep(x) (a[(x)].dep)
#define top(x) (a[(x)].top)
#define dfn(x) (a[(x)].dfn)
#define fa(x) (a[(x)].fa)
#define sz(x) (a[(x)].sz)
#define val(x) (a[(x)].val)
void dfs(int x, int fa) {
dep(x) = dep(fa) + 1;
fa(x) = fa; sz(x) = 1;
for (int it : G[x]) {
if (it == fa) continue;
dfs(it, x);
if (sz(son(x)) < sz(it)) son(x) = it;
sz(x) += sz(it);
}
}
void dfs(int x, int fa, int top) {
dfn(x) = ++id;
top(x) = top;
if (!son(x)) return;
dfs(son(x), x, top);
for (int it : G[x]) {
if (it == son(x) || it == fa) continue;
dfs(it, x, it);
}
}
vector<pii> A,B,C;
void modify_chain(int x,int y,vector<pii> &res){
while(top(x)!=top(y)){
if(dep(top(x))<dep(top(y))) swap(x,y);
res.push_back({dfn(top(x)),dfn(x)});
x=fa(top(x));
}
if(dep(x)>dep(y)) swap(x,y);
res.push_back({dfn(x),dfn(y)});
}
vector<pii> U(vector<pii> res){
vector<pii> ans;
ans.push_back(res[0]);
for(int i=1;i<res.size();i++){
if(res[i].first>res[i-1].second+1) ans.push_back(res[i]);
else ans.back().second=max(res[i].second,ans.back().second);
}
return ans;
}
int main(){
IOS;
int T;
cin>>T;
while(T--){
id=0;
cin>>n>>m;
for(int i=1;i<=n;i++) G[i].clear();
for(int i=1;i<n;i++){
int x;
cin>>x;
G[x].push_back(i+1);
}
dfs(1,0);
dfs(1,0,1);
while(m--){
int k1,k2,k3;
cin>>k1>>k2>>k3;
A.clear();B.clear();C.clear();
for(int i=1;i<=k1;i++){
int x;
cin>>x;
modify_chain(1,x,A);
}
for(int i=1;i<=k2;i++){
int x;
cin>>x;
modify_chain(1,x,B);
}
for(int i=1;i<=k3;i++){
int x;
cin>>x;
C.push_back({dfn(x),dfn(x)+sz(x)-1});
}
sort(A.begin(),A.end());
sort(B.begin(),B.end());
sort(C.begin(),C.end());
// A=U(A);B=U(B);C=U(C);集合内线段合并
vector<pii>tot;
//second为012表示为起始点 345表示为结束点
//排序后 先插入再删除
for(auto it:A){
tot.push_back({it.first,0});
tot.push_back({it.second,3});
}
for(auto it:B){
tot.push_back({it.first,1});
tot.push_back({it.second,4});
}
for(auto it:C){
tot.push_back({it.first,2});
tot.push_back({it.second,5});
}
sort(tot.begin(),tot.end());
int st[3]={0,0,0},ans=0;
int sz=0;
for(int i=0;i<tot.size();i++){
if(st[1]>0&&st[2]>0&&st[0]>0)
sz+=tot[i].first-tot[i-1].first;
if(tot[i].second<3){
st[tot[i].second]++;
if(st[1]>0&&st[2]>0&&st[0]>0) if(!sz) sz=1;
}else{
st[tot[i].second-3]--;
if(!st[1]||!st[2]||!st[0]){
ans+=sz;sz=0;
}
}
}
cout<<ans<<endl;
}
}
return 0;
}
方案二
先计算每个点可以到达的点有多少, A A A集合中的每个点按照 d f s dfs dfs序排序,那么 A A A集合中总共能到达的点的数量即为每个点可以到达的点的数量每减去相邻两点的 L C A LCA LCA能到达的点的数量(这里是重点!!)。对 B B B集合的点做同样的处理,然后将 A , B A,B A,B集合合并,减去合并后的集合能到达的点,得到的就是 A , B A,B A,B集合中的点都能到的点的数量。(也就是求 A ∩ B = A + B − A ∪ B A\cap B=A+B-A\cup B A∩B=A+B−A∪B)
将 C C C中同一链上的点只保留最高点(也就是删除所有在 C C C标记点子树里的 C C C标记点),此过程可以用 d f s dfs dfs序快速处理,然后处理每个 C C C标记节点子树中的 A , B A,B A,B即可。
struct edge{
int to,next;
}a[N];
int head[N],cnt,dep[N],f[N][20],n,m,dfn[N],id,sz[N];
void add(int x,int y){
a[++cnt]={y,head[x]};
head[x]=cnt;
}
void dfs(int x,int fa){
dep[x]=dep[fa]+1;dfn[x]=++id;
sz[x]=1;
f[x][0]=fa;
for(int i=1;i<=__lg(dep[x]);i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=head[x];i;i=a[i].next){
int y=a[i].to;
dfs(y,x);
sz[x]+=sz[y];
}
}
int LCA(int x,int y){
if(dep[y]>dep[x]) swap(x,y);
while(dep[x]>dep[y]) x=f[x][__lg(dep[x]-dep[y])];
if(x==y) return x;
for(int i=__lg(dep[x]);i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
int cal(vector<int> A,vector<int> C){
sort(A.begin(),A.end(),[](int x,int y){
return dfn[x]<dfn[y];
});//按dfs序排列
int res=0;
int l=0,r=0;
for(int it:C){//双指针 计算以it为根节点的子树中有多少AB能到的点
while(A.size()>l&&dfn[A[l]]<dfn[it]) l++;
r=l;
while(r<A.size()&&dfn[A[r]]<=dfn[it]+sz[it]-1) r++;
for(int i=l;i<r;i++) res+=dep[A[i]]-dep[it]+1;
for(int i=l;i<r-1;i++) res-=dep[LCA(A[i],A[i+1])]-dep[it]+1;
l=r;
}
return res;
}
int main(){
IOS;
int T;
cin>>T;
while(T--){
cin>>n>>m;
cnt=0;
for(int i=1;i<=n;i++) dep[i]=head[i]=0;
for(int i=1;i<n;i++){
int x;
cin>>x;
add(x,i+1);
}
dfs(1,0);
while(m--){
int k1,k2,k3;
cin>>k1>>k2>>k3;
vector<int> A,B,C,c;
for(int i=1;i<=k1;i++){
int x;
cin>>x;
A.push_back(x);
}
for(int i=1;i<=k2;i++){
int x;
cin>>x;
B.push_back(x);
}
for(int i=1;i<=k3;i++){
int x;
cin>>x;
C.push_back(x);
}
sort(C.begin(),C.end(),[](int x,int y){
return dfn[x]<dfn[y];
});
int las=0;
for(int it:C){//删除不必要的点
if(dfn[las]+sz[las]-1>=dfn[it]) continue;
c.push_back(it);las=c.back();
}
//删除后保留的点放入c中
int res=cal(A,c)+cal(B,c);
for(int it:B) A.push_back(it);
res-=cal(A,c);//减去交集
cout<<res<<endl;
}
}
return 0;
}
Assassination
题意
给定一个图,求其最少删除多少条边才能使得最大生成树的值发生改变
思路
引入生成树中的关键边和伪关键边概念。
关键边即为删除后会使最大/小生成树的值发生改变的边,伪关键边为其会出现在最大/小生成树中,但不会出现在所有的最大/小生成树中的边。
具体请参考这篇
到这里此题就可以将每条边按照求最大生成树的方案,依次加入不同权值的边,由于对于 k r u s k a l kruskal kruskal生成树过程中相同权值的边处理完成后,集合的状态是确定的(假如现在已经处理完成了所有权值大于k的边,此时图的连通状态时确定的)。对于下一个要加入的边,假设其权值为k,若某边的两端点属于同一集合,其必定不产生贡献,对于所有连接两个不同集合的权值为k的边,若将原有的连通块看作一个点,则新生成的图中若存在桥,则必定删除桥就可以得到答案(桥即为关键边),否则就要在新的图上求全局最小割(此时每个边的边权看作1,此时求出来的就是最少要删除的边数),最后对每次合并时的最小割取min即可。
吐槽
题解还使用Tarjan判断是否存在桥(即关键边),我认为并不需要此操作,直接求全局最小割即可,因为若存在桥,全局最小割的结果必定是1。
最后: 数据有点水,原代码有点神奇的写法都水过了
map<int,vector<int> > G;
vector<int> valid_edge,sub_edge[N];
int n,m,f[N],is_key[N];
pii E[N];
int find(int x){
return f[x]==x?x:f[x]=find(f[x]);
}
struct bridge{//Tarjan
int n,m,dfn[N],low[N],num,id,pos[N],p[N];
vector<pii> g[N];
void build_Graph(vector<int> _edge){
//新建图
num=0;id=0;
vector<int> tmp;
for(int it:_edge){
tmp.push_back(E[it].first);
tmp.push_back(E[it].second);
}
sort(tmp.begin(),tmp.end());
tmp.erase(unique(tmp.begin(),tmp.end()),tmp.end());
//点离散化
for(int i=0;i<tmp.size();i++) pos[i+1]=tmp[i],p[tmp[i]]=i+1;
n=tmp.size();m=_edge.size();
for(int i=1;i<=n;i++) g[i].clear();
for(int it:_edge){
g[p[E[it].first]].push_back({p[E[it].second],it});
g[p[E[it].second]].push_back({p[E[it].first],it});
}
for(int i=1;i<=n;i++){
if(!dfn[i]) Tarjan(i,0);
}
}
void Tarjan(int x,int fa){
dfn[x]=low[x]=++id;
for(auto it:g[x]){
if(it.first==fa) continue;
if(!dfn[it.first]){
Tarjan(it.first,x);
low[x]=min(low[x],low[it.first]);
if(low[it.first]>dfn[x]) is_key[it.second]=1;
}else if(!is_key[it.second])
low[x]=min(low[x],dfn[it.first]);
}
}
}Tar;
struct Gmin {//全局最小割
int n, m;
struct edge {
int to, next, w;
};
edge a[N << 1];
int f[N], cnt, head[N], link[N], val[N],pos[N],p[N];
bool vis[N],ok;
void add(int x, int y, int z) {
a[++cnt] = { y,head[x],z };
head[x] = cnt;
}
//初始化建图
void init(vector<int> _edge) {
ok=0;
vector<int> tmp;
for(int it:_edge){
if(is_key[it]) {ok=1;return;}
tmp.push_back(E[it].first);
tmp.push_back(E[it].second);
}
if(!tmp.size()) {ok=1;return ;}//ok表示存在关键边
sort(tmp.begin(),tmp.end());
tmp.erase(unique(tmp.begin(),tmp.end()),tmp.end());
for(int i=0;i<tmp.size();i++) pos[i+1]=tmp[i],p[tmp[i]]=i+1;
n=tmp.size();m=_edge.size();
memset(head+1,0,sizeof(int)*n);
memset(link+1,0,sizeof(int)*n);
for (int i = 1; i <= n; i++) f[i] = i;
cnt = 0;
for(int it:_edge){
if(is_key[it]) continue;
add(p[E[it].first],p[E[it].second],1);
add(p[E[it].second],p[E[it].first],1);
}
}
int find(int x) {
return f[x] == x ? x : f[x] = find(f[x]);
}
void merge(int u, int v) {
int i = u;
while (link[i]) i = link[i];
//模拟链表记录点集内的点
link[i] = v;
f[v] = u;
}
int MCP(int num, int& s, int& t) {
memset(val+1, 0, sizeof(int)*n);
//val[i]为点集A到点i的边权总和
memset(vis+1, 0, sizeof(int)*n);
//标记当前点是否在点集A内
priority_queue<pii> q;
t = 1;//t为链表头
while (--num) {
s = t;
vis[s] = 1;
for (int x = s; x; x = link[x]) {
for (int i = head[x]; i; i = a[i].next) {
int v = find(a[i].to);
if (!vis[v]) {
val[v] += a[i].w;
//更新A点集到v点的边权和
q.push({ val[v],v });
}
}
}
t = 0;
while (!t) {
if (!q.size()) return 0;
pii tmp = q.top();
q.pop();
if (val[tmp.second] == tmp.first) t = tmp.second;
//确保队列中的val都是该点的最后更新的值
}
}
return val[t];
}
int StoerWagner() {
if(ok) return 1;//存在关键边 答案为1
int res = inf;
for (int i = n; i > 1; i--) {
//n-1次类似prim算法
int s, t;
res = min(res, MCP(i, s, t));
if (res == 0) break;
merge(s, t);//合并最后两点
}
return res;
}
}sub_G;
int main(){
IOS;
int T;
cin>>T;
while(T--){
cin>>n>>m;
G.clear();//清空边
for(int i=1;i<=n;i++) f[i]=i;
memset(is_key+1,0,sizeof (int)*m);
//关键边标记清空
for(int i=1;i<=m;i++){
int x;
cin>>E[i].first>>E[i].second>>x;//读入边
if(E[i].first==E[i].second) continue;
G[x].push_back(i);
}
int ans=inf;//答案
for(auto it=G.rbegin();it!=G.rend();it++){
//倒着遍历
valid_edge.clear();
for(auto i:it->second){//权值为it.first的边
E[i].first=find(E[i].first);
E[i].second=find(E[i].second);
if(E[i].first==E[i].second) continue;
valid_edge.push_back(i);
}
if(!valid_edge.size()) continue;
Tar.build_Graph(valid_edge); //Tarjan不必要
for(int it:valid_edge) f[find(E[it].first)]=find(E[it].second);
//点合并
for(int it:valid_edge){//划分连通块
if(is_key[it]) continue;
int f1=find(E[it].first);
sub_edge[f1].push_back(it);
}
for(int it:valid_edge){//对每个连通块求全局最小割
int f1=find(E[it].first);
if(sub_edge[f1].size()==0) continue;
sub_G.init(sub_edge[f1]);
int tmp=sub_G.StoerWagner();
ans=min(ans,tmp);
sub_edge[f1].clear();
}
}
cout<<ans<<endl;
}
return 0;
}
DOS Card
题意
给定n个数字,每次询问一个区间 [ l , r ] [l,r] [l,r],求区间最大的 a i 1 2 − a j 1 2 + a i 2 2 − a j 2 2 a_{i1}^2-a_{j1}^2+a_{i2}^2-a_{j2}^2 ai12−aj12+ai22−aj22 其中 i 1 < j 1 , i 2 < j 2 i1<j1,i2<j2 i1<j1,i2<j2。
思路
线段树维护区间最大值、次大值、最小值、次小值、选择一对的最大值、选择一对和一个减去的数的最大值,选择一对和加上一个数的最大值。
合并时需要注意一些细节。
ll a[N];
struct node{
ll max1,max2,min1,min2,cnt1,cnt2,one_max,one_min;
node(){
max1=max2=cnt1=cnt2=one_max=one_min=-INF;
min1=min2=INF;
}
void init(){
max1=max2=cnt1=cnt2=one_max=one_min=-INF;
min1=min2=INF;
}
node operator+(const node A){
node res;
res.max1=max({max1,A.max1});
res.max2=max({max2,A.max2,min(max1,A.max1)});
res.min1=min(min1,A.min1);
res.min2=min({min2,A.min2,max(min1,A.min1)});
res.cnt1=max({cnt1,A.cnt1,max1-A.min1});
res.one_max=max({one_max,A.one_max,max1+max2-A.min1,cnt1+A.max1});
if(A.cnt1!=-INF) res.one_max=max(res.one_max,max1+A.max1-A.min1);
res.one_min=max({one_min,A.one_min,max1-A.min1-A.min2,A.cnt1-min1});
if(cnt1!=-INF) res.one_min=max(res.one_min,max1-min1-A.min1);
res.cnt2=max({max1+A.one_min,max1+max2-A.min1-A.min2,one_max-A.min1,cnt2,A.cnt2,
cnt1+A.cnt1});
return res;
}
}tr[N];
void build(int now,int l,int r){
tr[now].init();
if(l==r){
tr[now].max1=tr[now].min1=a[l];
return;
}
int mid=l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
tr[now]=tr[ls]+tr[rs];
}
node query(int now,int l,int r,int L,int R){
if(L<=l&&r<=R) return tr[now];
int mid=l+r>>1;
if(L<=mid&&R>mid) return query(ls,l,mid,L,R)+query(rs,mid+1,r,L,R);
else if(L<=mid) return query(ls,l,mid,L,R);
return query(rs,mid+1,r,L,R);
}
int main(){
IOS;
int T;
cin>>T;
while(T--){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i],a[i]*=a[i];
build(1,1,n);
while(m--){
int l,r;
cin>>l>>r;
cout<<query(1,1,n,l,r).cnt2<<endl;
}
}
return 0;
}