[Solution] [COCI 2023/2024 #1] Mostovi

· · 题解

Solution

一个唯一优势大概只有想法可能较为自然的做法。

先观察部分分,m-n\le 20 提示性很强:如果放在生成树上,则说明非树边数量 \le 21;加之这是一个在图上没什么思路的问题,于是自然想到建出图的生成树。而对生成树计数考虑将树边和非树边分开处理。

注意,在整道题目中,通过 DFS 序生成树中所有返祖边为祖孙关系这个结论简化了很多分讨。

树边

一条树边 (x,y) 的删除会将整棵树划分成几个区域:x 子树外、x 其余子树、y 子树。其中 x 可能为根而不存在第一个区域,故考虑对 x 是否为根分讨。注意其他两个区域也可能不存在,但是在分讨后会发现可以被情况包含,这里跳过多余的情况。

非树边

形如:

画得有点花,这里解释一下。当前处理的是非树边 (x,y),除开 x,y 的所有点分成了 4 个可能的区域(绿色框圈出),蓝色虚线为区域之间可能的非树边情况,son_y 表示 yx 方向的子节点。考虑分讨 4 个区域的存在情况计数。

注意在处理 d_1,d_2 时可能的特殊情况:

时间复杂度 O(m\log n)

可能有更简单的判断方法,但考场上做得太红温了,只想到了这几个较为无脑的做法。

Code

看着有点长,但实际上核心计数分讨部分非常短,大部分码量在预处理,比如主席树板子就占了很大的篇幅。分讨完全、思路清晰之后写出来很快。

#include <bits/stdc++.h>
using namespace std;

char BEGIN;
namespace Cherry {
    const int N=1e5+10,M=3e5+10;
    int n,m,ans;
    int d[N],siz[N],rd[N],son[N],edf[N];
    int mn[N],mx[N],mx2[N];
    int Mn[N],Mx[N];
    bool vis[N];

    struct SegmentTree {
        int cnt;
        int rt[N];
        struct tree {
            int ls,rs,cnt;
        }tr[70*N];
        int new_node(int id) { return tr[++cnt]=tr[id],cnt; }
        void pushup(int id) { tr[id].cnt=tr[tr[id].ls].cnt+tr[tr[id].rs].cnt; }
        void modify(int &id,int l,int r,int x,int d) {
            id=new_node(id);
            if(l==r) return tr[id].cnt+=d,void();
            int mid=(l+r)/2;
            if(x<=mid) modify(tr[id].ls,l,mid,x,d);
            else modify(tr[id].rs,mid+1,r,x,d);
            pushup(id);
        }
        int merge(int x,int y,int l,int r) {
            if(!x||!y) return x|y;
            int id=++cnt;
            tr[id].cnt=tr[x].cnt+tr[y].cnt;
            if(l==r) return id;
            int mid=(l+r)/2;
            tr[id].ls=merge(tr[x].ls,tr[y].ls,l,mid);
            tr[id].rs=merge(tr[x].rs,tr[y].rs,mid+1,r);
            pushup(id);
            return id;
        }
        int query(int id1,int id2,int l,int r,int lt,int rt) {
            if(!id1&&!id2) return 0;
            if(l>=lt&&r<=rt) return tr[id2].cnt-tr[id1].cnt;
            int mid=(l+r)/2;
            if(lt<=mid&&query(tr[id1].ls,tr[id2].ls,l,mid,lt,rt)) return 1;
            if(rt>mid&&query(tr[id1].rs,tr[id2].rs,mid+1,r,lt,rt)) return 1;
            return 0;
        }
        int findl(int id,int l,int r,int lt,int rt) {
            if(!id||!tr[id].cnt) return 0;
            if(l==r) return l;
            int mid=(l+r)/2;
            if(rt<=mid) return findl(tr[id].ls,l,mid,lt,rt);
            if(tr[tr[id].ls].cnt) return findl(tr[id].ls,l,mid,lt,rt);
            return findl(tr[id].rs,mid+1,r,lt,rt);
        }
        int findr(int id,int l,int r,int lt,int rt) {
            if(!id||!tr[id].cnt) return 0;
            if(l==r) return l;
            int mid=(l+r)/2;
            if(rt<=mid) return findr(tr[id].ls,l,mid,lt,rt);
            int x=findr(tr[id].rs,mid+1,r,lt,rt);
            if(!x) x=findr(tr[id].ls,l,mid,lt,rt);
            return x;
        }
    }T;
    bool check(int x,int y,int d1,int d2) { return T.query(T.rt[y],T.rt[x],1,n,d1,d2); } //判断 x 子树除去 y 后在 [d1,d2] 深度中是否有连边

    struct edge {
        int y,f,nextt;
    }e[2*M];
    int tot;
    int elast[N];
    void add(int x,int y) {
        e[++tot].y=y;
        e[tot].nextt=elast[x];
        elast[x]=tot;
    }

    void dfs(int x,int fa) {
        d[x]=d[fa]+1,siz[x]=1,mn[x]=n+1;
        for(int i=elast[x];i;i=e[i].nextt) {
            int y=e[i].y;
            if(y==fa) continue;
            if(!d[y]) {
                rd[x]++,rd[y]++,e[i].f=1;
                dfs(y,x);
                siz[x]+=siz[y];
                mn[x]=min(mn[x],mn[y]); //子树中连到最上方的非树边端点
                if(mn[y]>mx[x]) mx2[x]=mx[x],mx[x]=mn[y]; //子树 mn 最大次大值
                else if(mn[y]>mx2[x]) mx2[x]=mn[y];
            }
            else mn[x]=min(mn[x],d[y]);
            if(d[x]>d[y]) edf[x]++;
        }
    }
    void dfs2(int x,int fa) { //主席树维护子树内非树边端点
        for(int i=elast[x];i;i=e[i].nextt) {
            if(!e[i].f) continue;
            int y=e[i].y;
            dfs2(y,x);
            T.rt[x]=T.merge(T.rt[x],T.rt[y],1,n);
        }
        for(int i=elast[x];i;i=e[i].nextt) {
            int y=e[i].y;
            if(y==fa||e[i].f||d[x]<d[y]) continue;
            T.modify(T.rt[x],1,n,d[y],1);
        }
    }

    int get(int x,int y) { return (mn[y]!=mx[x]?mx[x]:mx2[x]); } //x 除开 y 方向的子节点中子树最浅非树边端点的最深深度
    void solve1(int x) { //树边
        for(int i=elast[x];i;i=e[i].nextt) {
            if(!e[i].f) continue;
            int y=e[i].y;
            if(x!=1) {
                if(siz[y]>1&&mx[y]>=d[x]) ans++; //y 子树无法连接 x 上方的连通块
                else if(siz[x]>siz[y]+1&&get(x,y)>=d[x]) ans++; //其余子节点无法连接 x 上方的连通块
            }
            else if(x==1&&rd[x]+rd[y]>3) ans++;
            solve1(y);
        }
    }

    struct BIT1 {
        int tr[N];
        int lowbit(int x) { return x&-x; }
        void add(int x,int d) { for(int i=n-x+1;i<=n;i+=lowbit(i)) tr[i]+=d; }
        int query(int x,int res=0) {
            for(int i=n-x+1;i>=1;i-=lowbit(i)) res+=tr[i];
            return res;
        }
    }Td;
    struct BIT2 {
        int tr[N];
        int lowbit(int x) { return x&-x; }
        void add(int x,int d) { for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=d; }
        int query(int x,int res=0) {
            for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];
            return res;
        }
    }Tu;

    void solve2(int x,int fa) { //非树边
        for(int i=elast[x];i;i=e[i].nextt) {
            if(!e[i].f) continue;
            son[x]=e[i].y;
            solve2(e[i].y,x);
        }
        if(!edf[x]) return;
        bool flag=0;
        for(int i=elast[x];i;i=e[i].nextt) { //判断 x 不同子树向上非树边情况
            if(!e[i].f) continue;
            int y=e[i].y;
            Mn[y]=T.findl(T.rt[y],1,n,1,d[x]-1); //最浅和最深的端点
            Mx[y]=T.findr(T.rt[y],1,n,1,d[x]-1);
            if(!Mn[y]) flag=1;
            else Td.add(Mx[y]-1,1),Tu.add(Mn[y]+1,1); //Td/Tu 维护若删除的是深度 d 的点,则有多少 x 的子树在 d 上/下方有连边
            if(Mn[y]==Mx[y]) vis[Mn[y]]=1; //注意特判子树内只有一条连接 x 上方的非树边的情况
        }
        for(int i=elast[x];i;i=e[i].nextt) {
            int y=e[i].y;
            if(y==fa||e[i].f||d[x]<d[y]) continue;
            if(flag) ans++;
            else if(vis[d[y]]) ans++;
            else if(n==siz[y]&&siz[x]==1) {
                if(rd[y]>1) ans++;
            }
            else if(n==siz[y]) {
                if(rd[y]>1||Td.query(d[y])<rd[x]-1) ans++;
            }
            else if(siz[x]==1) {
                if(!check(son[y],x,1,d[y]-1)||(rd[y]>2&&get(y,son[y])>=d[y])) ans++;
            }
            else {
                if(rd[y]>2&&get(y,son[y])>=d[y]) ans++;
                else if(check(son[y],x,1,d[y]-1)+Td.query(d[y])+Tu.query(d[y])<rd[x]) ans++; //边数 >=rd[x] 且所有连通块 rd!=0 -> rd[x]+1 个联通块联通
            }
        }
        for(int i=elast[x];i;i=e[i].nextt) {
            if(!e[i].f) continue;
            int y=e[i].y;
            if(Mn[y]) Td.add(Mx[y]-1,-1),Tu.add(Mn[y]+1,-1);
            vis[Mn[y]]=0;
        }
    }

    int main() {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++) {
            int x,y;
            scanf("%d%d",&x,&y);
            add(x,y),add(y,x);
        }
        dfs(1,0),dfs2(1,0),solve1(1),solve2(1,0);
        printf("%d",ans);

        return 0;
    }
}
char END;

int main() {
//    freopen("bridge.in","r",stdin);
//    freopen("bridge.out","w",stdout);
    clock_t begin=clock();
    Cherry::main();
    clock_t end=clock();
    fprintf(stderr,"Time: %.3lf/1 s\n",(end-begin)/1000.0);
    fprintf(stderr,"Memory: %.3lf/512 MB\n",abs(&BEGIN-&END)/1024.0/1024.0);

    return 0;
}
//g++ bridge.cpp -o bridge -std=c++14 -Wall -O2 "-Wl,--stack=2000000000"

附赠一些小样例:

8 9
1 4
2 7
1 2
1 3
2 4
2 5
4 6
5 7
5 8
Out: 6

5 7
1 3
2 4
3 5
1 2
2 3
3 4
4 5
Out: 2

6 8
1 6
2 4
3 5
1 2
2 3
3 4
4 5
5 6
Out: 0

6 8
1 6
2 4
3 5
2 3
3 4
4 5
5 6
4 6
Out: 4

7 10
1 2
2 3
2 4
3 5
4 6
1 7
1 4
1 5
5 4
3 7
Out: 3

6 8
1 2
2 3
3 4
2 5
4 6
6 1
5 1
2 4
Out: 2

10 13
1 2
2 3
1 4
3 5
5 6
3 7
2 8
8 9
8 10
8 1
10 4
9 10
1 9
Out: 6