Codeforces - 图论题目(难度:2200)

Codeforces - 图论题目(难度:2200)

AC 之后不写题解,赛前火葬场!!

1. 507E - Breaking Good(最短路 + 松弛条件改造)

题目链接:https://codeforces.com/contest/507/problem/E

1.1 题意

给你一个 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n\le 10^5) n(2n105) 个点和 m ( 0 ≤ m ≤ min ⁡ ( n ( n − 1 ) 2 , 1 0 5 ) ) m(0 \le m\le \min(\frac{n(n-1)}{2},10^5)) m(0mmin(2n(n1),105)) 条边的无向连通图,保证没有自环和重边。所有边的长度均为 1 1 1,每一条边 i i i 都有一个状态 w i ( w i ∈ [ 0 , 1 ] ) w_i(w_i \in [0,1]) wi(wi[0,1]),状态为 0 0 0 表明这条边未在工作,状态为 1 1 1 表明这条边正在工作。

你可以进行若干次操作,每次操作可以将一条边的状态改变。

现在要从 1 1 1 点走到 n n n 点,使得最短路上面的边都处于工作状态,最短路之外的边都处于非工作状态。

问最少的操作次数。

1.2 解题过程

显然,本题的所有操作需要在保证最短路的基础上进行。

因此我们通过 Dijkstra 跑最短路,记录两个变量 d [ i ] d[i] d[i] 表示 1 1 1 i i i 的最短路距离, l e n [ i ] len[i] len[i] 表示从 1 1 1 走最短路走到 i i i,最大的处于工作状态的道路数量。

之后在松弛 ( u , v , w ) (u,v,w) (u,v,w) 时,分两种情况:

  1. d [ v ] > d [ u ] + 1 d[v] > d[u] + 1 d[v]>d[u]+1 时,按照传统 Dijkstra 的思路,更新 d [ v ] d[v] d[v],同时需要更新 l e n [ v ] = l e n [ u ] + w len[v] = len[u] +w len[v]=len[u]+w,并将 v v v 加入到堆中;
  2. d [ v ] = d [ u ] + 1 d[v]=d[u]+1 d[v]=d[u]+1 时,检查是否满足 l e n [ v ] < l e n [ u ] + w len[v]len[v]<len[u]+w,若满足则更新 l e n [ v ] len[v] len[v]

松弛时记录每个点的前驱边,进行完 Dijkstra 之后直接处理答案即可。

时间复杂度: O ( ( n + m ) log ⁡ n ) O((n+m) \log n) O((n+m)logn)

1.3 代码

int head[maxn], n, cnt, d[maxn], from[maxn];
int len[maxn];
bool mark[2 * maxn], vis[maxn];
struct edge
{
    int u, v, nxt;
    int w;
} Edge[2 * maxn];
struct node
{
    int d, id, len;
    node (int _d, int _id, int _len):
        d(_d), id(_id), len(_len) {}
    const bool operator < (const node b) const
    {
        return d > b.d || (d == b.d && len < b.len);
    }
};
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}
void addedge(int u, int v, int w)
{
    Edge[cnt].u = u;
    Edge[cnt].v = v;
    Edge[cnt].w = w;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
priority_queue<node> que;
void Dijkstra()
{
    memset(d, 0x3f, sizeof(d));
    d[1] = 0;
    que.push(node(d[1], 1, len[1]));
    while(!que.empty())
    {
        node now = que.top();
        que.pop();
        if(vis[now.id]) continue;
        vis[now.id] = true;
        for(int i = head[now.id]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            if(d[v] > d[now.id] + 1)
            {
                d[v] = d[now.id] + 1;
                len[v] = len[now.id] + Edge[i].w;
                que.push(node(d[v], v, len[v]));
                from[v] = i;
            }
            else if(d[v] == d[now.id] + 1)
            {
                if(len[v] < len[now.id] + Edge[i].w)
                {
                    len[v] = len[now.id] + Edge[i].w;
                    from[v] = i;
                }
            }
        }
    }
}
vector<pair<pii, int> > ans;
int main()
{
    int m, u, v, w;
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        addedge(u, v, w);
        addedge(v, u, w);
    }
    Dijkstra();
    int pos = n;
    while(pos != 1)
    {
        int id = from[pos];
        mark[id] = mark[id ^ 1] = true;
        pos = Edge[id].u;
    }
    for(int i = 0; i < cnt; i += 2)
    {
        u = Edge[i].u;
        v = Edge[i].v;
        w = Edge[i].w;
        if(w == 0 && mark[i])   ans.pb(make_pair(pii(u, v), 1));
        else if(w == 1 && mark[i] == false) ans.pb(make_pair(pii(u, v), 0));
    }
    printf("%d\n", (int)ans.size());
    for(int i = 0; i < ans.size(); i++)
    {
        pair<pii, int> now = ans[i];
        printf("%d %d %d\n", now.first.first, now.first.second, now.second);
    }
    return 0;
}

2. 915D - Almost Acyclic Graph(拓扑排序)

题目链接:https://codeforces.com/problemset/problem/915/D

2.1 题意

给你一个 n ( 2 ≤ n ≤ 500 ) n(2 \le n \le 500) n(2n500) 个点和 1 ≤ m ≤ min ⁡ ( n ( n − 1 ) , 1 0 5 ) 1 \le m\le \min(n(n-1),10^5) 1mmin(n(n1),105) 条边的有向图,保证不含自环和重边。

现在允许你最多删除一条边,问是否可以使整个图形成一个有向无环图。

2.2 解题过程

判定有向无环图,只需通过拓扑排序,若拓扑排序成功,证明为有向无环图。

一个显然的思路是,枚举要删的边,之后跑一遍拓扑排序来 check,但这样做的时间复杂度是 O ( m ⋅ ( n + m ) ) O(m \cdot (n+m)) O(m(n+m)) 的,显然会超时,因此我们考虑换一种策略。

删除一条边,等价于使一个点的入度减 1 1 1。而我们注意到 n n n 很小,因此我们直接枚举入度减 1 1 1 的点,将该点的入度减 1 1 1 之后去 check,时间复杂度为 O ( n ⋅ ( n + m ) ) O(n \cdot (n+m)) O(n(n+m)),是可以接受的。

注意要在不删边的情况下先跑一遍拓扑排序。

2.3 错误点

  1. 没有在不删边的情况下先跑一遍拓扑排序。
  2. 每次进行完 check 后没有恢复每个点原始的入度。

2.4 代码

int head[maxn], n, cnt, indeg[maxn], indeg2[maxn];
int topo[maxn];
struct edge
{
    int v, nxt;
    ll length;
} Edge[maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
bool toposort()
{
    int pos = 0;
    queue<int> que;
    for(int i = 1; i <= n; i++)
    {
        if(!indeg[i])   que.push(i);
    }
    while(!que.empty())
    {
        int now = que.front();
        que.pop();
        topo[++pos] = now;
        for(int i = head[now]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            if(v == now)    continue;
            --indeg[v];
            if(!indeg[v])   que.push(v);
        }
    }
    return pos == n;
}
int main()
{
    int m, u, v;
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        indeg[v]++;
    }
    memcpy(indeg2, indeg, sizeof(indeg));
    bool isok = toposort();
    for(int j = 1; j <= n; j++) indeg[j] = indeg2[j];
    for(int i = 1; i <= n; i++)
    {
        indeg[i]--;
        if(toposort())
        {
            isok = true;
            break;
        }
        for(int j = 1; j <= n; j++) indeg[j] = indeg2[j];
    }
    if(isok)    printf("YES\n");
    else printf("NO\n");
    return 0;
}

3. 788B - Weird journey(欧拉回路)

题目链接:https://codeforces.com/problemset/problem/788/B

3.1 题意

给你一个 n ( 1 ≤ n ≤ 1 0 6 ) n(1 \le n \le 10^6) n(1n106) 个点和 m ( 1 ≤ m ≤ 1 0 6 ) m (1 \le m \le 10^6) m(1m106) 条边的无向图,保证没有重边,但可能有自环。

一条路径被称为好的,当且仅当这条路径经过 m − 2 m-2 m2 条边两次,并经过 2 2 2 条边恰好一次。

问好的路径的方案数。

3.2 解题过程

如果我们把每一条边都复制一份的话,会发现每个点的度数一定为偶数,因此所有的边构成了一个欧拉回路。

此时问题就转化为在一个欧拉回路上删掉两条边,使得剩下的边构成一个欧拉通路。

一个图构成一个欧拉通路,当且仅当奇度顶点的数量为 0 0 0 2 2 2

设自环的数量为 n u m num num

枚举要删除的第一个边 i i i,分几种情况:

  1. i i i 为自环,因此删除 i i i 不影响度数的奇偶性,此时第二条边可以任意删除,故答案加 m − 1 m-1 m1
  2. i i i 不是自环,此时删边 i ( u , v ) i(u,v) i(u,v) 会影响点 u u u 和点 v v v 度数的奇偶性,因此第二条边要么删除相邻的边,要么删除一个自环。因此答案加上 ( d e g [ u ] − 1 ) + ( d e g [ v ] − 1 ) + n u m (deg[u]-1) + (deg[v]-1) + num (deg[u]1)+(deg[v]1)+num

这时我们会发现,每种方案都被计算了两次,因此最终的答案为方案数除以 2 2 2

特别地,如果图不连通,则答案一定为 0 0 0

时间复杂度: O ( n + m ) O(n+m) O(n+m)

3.3 错误点

  1. 判断图联通时,可以先标记有效点(即有边的点的个数),之后从任意一个有效点开始 DFS,并进行点的标记。最后如果被标记的点的个数等于有效点的个数,则表明该图为连通图。这种方式错误率比较低。
  2. 要防止出现某些方案被计算两次,而另一些方案被计算一次的情况。

3.4 代码

int n, cnt;
int head[maxn];
struct edge
{
    int u, v, nxt;
} Edge[2 * maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].u = u;
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
int deg[maxn];
bool vis[maxn];
void dfs(int id)
{
    vis[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(vis[v])  continue;
        dfs(v);
    }
}
int main()
{
    int m, u, v;
    scanf("%d%d", &n, &m);
    init();
    int circle = 0;
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        vis[u] = vis[v] = true;
        if(u != v)
        {
            addedge(v, u);
            deg[v]++;
            deg[u]++;
        }
        if(u == v)  circle++;
    }
    int num = 0;
    for(int i = 1; i <= n; i++) num += vis[i];
    for(int i = 1; i <= n; i++)
    {
        if(vis[i])
        {
            memset(vis, false, sizeof(vis));
            dfs(i);
            break;
        }
    }
    int tot = 0;
    for(int i = 1; i <= n; i++) tot += vis[i];
    if(tot < num)
    {
        printf("0\n");
        return 0;
    }
    ll ans = 0;
    for(int i = 0; i < cnt; i++)
    {
        u = Edge[i].u;
        v = Edge[i].v;
        if(u == v)  ans += 1LL * (m - 1);
        else
        {
            ans += 1LL * circle;
            ans += 1ll * (deg[u] - 1);
            ans += 1ll * (deg[v] - 1);
            i++;
        }
    }
    printf("%lld\n", ans / 2);
    return 0;
}

4. 11D - A Simple Task(状压 dp)

题目链接:https://codeforces.com/problemset/problem/11/D

4.1 题意

给你一个 n ( 1 ≤ n ≤ 19 ) n(1 \le n \le 19) n(1n19) 个点和 m ( m ≥ 0 ) m(m \ge 0) m(m0) 条边的无向图,保证不含自环和重边。

计算图中简单环的个数。

简单环定义为不含重复的顶点和边的环。

4.2 解题过程

首先考虑一个 n n n 元环可以有 n n n 个起点,为了防止重复计算,我们设一个环的起点为其内部编号最小的点。

因为 n n n 很小,所以可以考虑状压。

d p [ S ] [ i ] dp[S][i] dp[S][i] 表示以 l o w b i t ( S ) lowbit(S) lowbit(S) 对应的点为起点, i i i 为终点的路径的方案数。

如果 i i i 是以 1 1 1 为起始下标的话,边界值为 d p [ 1 < < ( i − 1 ) ] [ i ] = 1 dp[1 << (i-1)][i] = 1 dp[1<<(i1)][i]=1

之后枚举 S S S 和当前终点 u u u 进行转移,在此基础上枚举 u u u 的邻接点 v v v,如果出现 v = l o w b i t ( S ) v=lowbit(S) v=lowbit(S),证明找到了环,将 d p [ S ] [ u ] dp[S][u] dp[S][u] 加入到答案中;否则进行转移 d p [ S ∣ ( 1 < < ( v − 1 ) ] [ v ] = d p [ S ∣ ( 1 < < ( v − 1 ) ] [ v ] + d p [ S ] [ u ] dp[S | (1 << (v-1)][v] = dp[S | (1 << (v-1)][v] + dp[S][u] dp[S(1<<(v1)][v]=dp[S(1<<(v1)][v]+dp[S][u]

时间复杂度: O ( 2 n ⋅ ( n + m ) ) O(2^n \cdot (n+m)) O(2n(n+m))

4.3 代码

int n, m, cnt;
int head[maxn];
ll dp[1 << 20][25];
struct edge
{
    int v, nxt;
} Edge[2 * maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
int lowbit(int x)
{
    return x & (-x);
}
int main()
{
    int u, v;
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        addedge(v, u);
    }
    ll ans = 0;
    for(int i = 1; i <= n; i++) dp[1 << (i - 1)][i] = 1;
    for(int i = 0; i < (1 << n); i++)
    {
        for(int j = 1; j <= n; j++)
        {
            if(!((i >> (j - 1)) & 1))   continue;
            for(int k = head[j]; k != -1; k = Edge[k].nxt)
            {
                int v = Edge[k].v;
                if((1 << (v - 1)) < lowbit(i))    continue;
                if(((i >> (v - 1)) & 1) && ((1 << (v - 1)) == lowbit(i)))  ans += dp[i][j];
                else dp[i + (1 << (v - 1))][v] += dp[i][j];
            }
        }
    }
    printf("%lld\n", (ans - m) / 2);
    return 0;
}

5. 715B - Complete The Graph(最短路)

题目链接:https://codeforces.com/problemset/problem/715/B

5.1 题意

给你一个 n ( 0 ≤ n ≤ 1000 ) n(0 \le n \le 1000) n(0n1000) 个点和 1 ≤ m ≤ 1 0 4 1 \le m \le 10^4 1m104 条边的无向图,保证没有自环和重边。

每条边都有边权 w i ( 1 ≤ w i ≤ 1 0 9 ) w_i(1 \le w_i \le 10^9) wi(1wi109),但一些边权丢失了(标记为 0 0 0)。

现在要对丢失边权的边重新分配一个正的边权,问是否存在一种方案,使得从 s s s t t t 的最短路的长度恰好为 L L L

5.2 解题过程

先假设丢失边权的边不存在,从 s s s 出发跑一遍 Dijkstra。

之后如果 d [ t ] < L d[t] < L d[t]<L,证明无论如何分配边权都无法达到要求,输出无解。

之后从 t t t 出发倒着跑 Dijkstra,贪心地将边权分配即可。

因为每条边的边权至少为 1 1 1,所以极有可能导致 d ′ [ s ] > L d'[s]>L d[s]>L,此时依然无解。

否则输出每条边的分配情况即可。

时间复杂度: O ( ( n + m ) log ⁡ n ) O((n+m)\log n) O((n+m)logn)

5.3 代码

int n, cnt, s, t;
int head[maxn];
bool vis[maxn], vis2[maxn];
ll d[maxn], d2[maxn], l;
struct edge
{
    int u, v, nxt;
    ll w;
} Edge[20 * maxn];
struct node
{
    ll d;
    int id;
    node(ll _d, int _id):
        d(_d), id(_id) {}
    const bool operator < (const node b) const
    {
        return d > b.d;
    }
};
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}
void addedge(int u, int v, ll w)
{
    Edge[cnt].u = u;
    Edge[cnt].v = v;
    Edge[cnt].w = w;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
priority_queue<node> que;
void Dijkstra()
{
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    que.push(node(d[s], s));
    while(!que.empty())
    {
        node now = que.top();
        que.pop();
        if(vis[now.id]) continue;
        vis[now.id] = true;
        for(int i = head[now.id]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            ll w = Edge[i].w;
            if(!w)  continue;
            if(d[v] > d[now.id] + w)
            {
                d[v] = d[now.id] + w;
                que.push(node(d[v], v));
            }
        }
    }
}
void Dijkstra2()
{
    memset(d2, 0x3f, sizeof(d2));
    memset(vis, false, sizeof(vis));
    d2[t] = 0;
    que.push(node(d2[t], t));
    while(!que.empty())
    {
        node now = que.top();
        que.pop();
        if(vis[now.id]) continue;
        vis[now.id] = true;
        for(int i = head[now.id]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            ll w = Edge[i].w;
            if(!w)
            {
                w = Edge[i].w = Edge[i ^ 1].w = max(1LL, l - d[v] - d2[now.id]);
            }
            if(d2[v] > d2[now.id] + w)
            {
                d2[v] = d2[now.id] + w;
                que.push(node(d2[v], v));
            }
        }
    }
}
int main()
{
    int m, u, v;
    ll w;
    scanf("%d%d%lld%d%d", &n, &m, &l, &s, &t);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%lld", &u, &v, &w);
        addedge(u, v, w);
        addedge(v, u, w);
    }
    Dijkstra();
    if(d[t] < l)
    {
        printf("NO\n");
        return 0;
    }
    Dijkstra2();
    if(d2[s] != l)  printf("NO\n");
    else
    {
        printf("YES\n");
        for(int i = 0; i < cnt; i += 2)
        {
            printf("%d %d %lld\n", Edge[i].u, Edge[i].v, Edge[i].w);
        }
    }
    return 0;
}

6. 1100E - Andrew and Taxi(二分 + 拓扑排序)

题目链接:https://codeforces.com/problemset/problem/1100/E

6.1 题意

给你一个 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 个点和 m ( 1 ≤ m ≤ 1 0 5 ) m(1 \le m \le 10^5) m(1m105) 条边的有向图。

现在你需要选择一些边,将其方向反转,这个代价为边权 w i w_i wi

总的代价为所操作边的代价的最大值。

问最小的代价,使得整个图为有向无环图。

同时需要输出反转边的方案。

6.2 解题过程

总的代价为所操作边的代价的最大值。

这个条件促使我们向二分答案的方向去想。

二分所操作边的代价的最大值 x x x,之后将代价大于 x x x 的边加入,跑一遍拓扑排序,如果发现这个残余的图中没有环,证明这个代价可行(因为那些没有加入的边可以随意调整方向直到不存在环);如果有环,证明这个代价不可行,它没有能力使那些代价大于 x x x 的边反转。

之后就要考虑如何输出方案了。

我们基于这个二分出的 x x x,将代价大于 x x x 的边加入,再跑一遍拓扑排序。

之后枚举代价不超过 x x x 的边 < u , v > <u,v>,如果发现 u u u 的拓扑序大于 v v v 的拓扑序,证明如果不经反转便加入 < u , v > <u,v>,会形成一个有向环,所以此时 < u , v > <u,v> 必须反转;否则 < u , v > <u,v> 不反转。

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

6.3 错误点

  1. 二分下界是 0 0 0 而不是 1 1 1

6.4 代码

int head[maxn], n, m, cnt;
int indeg[maxn], outdeg[maxn], topo[maxn], pos[maxn];
struct edge
{
    int u, v, w, nxt;
} Edge[maxn], input[maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    memset(indeg, 0, sizeof(indeg));
    memset(topo, 0, sizeof(topo));
    cnt = 0;
}
void addedge(int u, int v, int w)
{
    Edge[cnt].v = v;
    indeg[v]++;
    Edge[cnt].w = w;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
bool toposort()
{
    queue<int> que;
    for(int i = 1; i <= n; i++)
    {
        if(!indeg[i])   que.push(i);
    }
    int tot = 0;
    while(!que.empty())
    {
        int now = que.front();
        que.pop();
        topo[++tot] = now;
        for(int i = head[now]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            if(--indeg[v] == 0)
            {
                que.push(v);
            }
        }
    }
    return tot == n;
}
bool check(int x)
{
    init();
    for(int i = 1; i <= m; i++)
    {
        if(input[i].w > x) addedge(input[i].u, input[i].v, input[i].w);
    }
    return toposort();
}
vector<int> ans;
int main()
    int u, v, w;
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        input[i] = (edge){u, v, w, -1};
    }
    int res = 0x3f3f3f3f;
    int left = 0, right = 1000000000;
    while(left <= right)
    {
        int mid = left + (right - left) / 2;
        if(check(mid))
        {
            res = min(res, mid);
            right = mid - 1;
        }
        else left = mid + 1;
    }
    init();
    for(int i = 1; i <= m; i++)
    {
        if(input[i].w > res) addedge(input[i].u, input[i].v, input[i].w);
    }
    toposort();
    for(int i = 1; i <= n; i++)
    {
        pos[topo[i]] = i;
    }
    for(int i = 1; i <= m; i++)
    {
        u = input[i].u;
        v = input[i].v;
        if(input[i].w <= res)
        {
            if(pos[u] > pos[v]) ans.pb(i);
        }
    }
    printf("%d %d\n", res, (int)ans.size());
    for(int i = 0; i < ans.size(); i++)
    {
        if(i > 0)
            printf(" ");
        printf("%d", ans[i]);
    }
    return 0;
}

7. 1029E - Tree with Small Distances(贪心)

题目链接:https://codeforces.com/problemset/problem/1029/E

7.1 题意

给你一棵 n ( 2 ≤ n ≤ 2 ⋅ 1 0 5 ) n(2 \le n \le 2 \cdot 10^5) n(2n2105) 个点的无向树,现在你需要添加一些边,使得 1 1 1 到任意点的最短路长度不超过 2 2 2

问最少添加的边数。

7.2 解题过程

先进行一波 DFS/BFS,处理出 1 1 1 到每个点的距离。

之后将那些距离大于 2 2 2 的点丢进一个 set 中,按照距离大的优先。

每次从 set 中拿出一个元素,答案加上 1 1 1,将其邻接的点全部移出 set

处理完 set 之后,便得到了答案。

7.3 代码

int n, cnt, ans;
int head[maxn], p[maxn], d[maxn];
struct edge
{
    int v, nxt;
} Edge[2 * maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0, ans = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void dfs(int id, int fa)
{
    p[id] = fa;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == fa) continue;
        d[v] = d[id] + 1;
        dfs(v, id);
    }
}
struct node
{
    int id;
    node (int _id):
        id(_id) {}
    const bool operator < (const node b) const
    {
        return d[id] > d[b.id] || (d[id] == d[b.id] && id < b.id);
    }
};
set<node> se;
int main()
{
    int u, v;
    scanf("%d", &n);
    init();
    for(int i = 1; i <= n - 1; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        addedge(v, u);
    }
    dfs(1, -1);
    for(int i = 1; i <= n; i++)
    {
        if(d[i] > 2)
        {
            se.insert(node(i));
        }
    }
    while(!se.empty())
    {
        node now = *se.begin();
        int id = p[now.id];
        if(se.find(node(id)) != se.end())  se.erase(node(id));
        ans++;
        for(int i = head[id]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            if(se.find(node(v)) != se.end())  se.erase(node(v));
        }
    }
    printf("%d\n", ans);
    return 0;
}

8. 1133F2 - Spanning Tree with One Fixed Degree(贪心)

题目链接:https://codeforces.com/problemset/problem/1133/F2

8.1 题意

给你一个 n ( 2 ≤ n ≤ 2 ⋅ 1 0 5 ) n(2 \le n \le 2 \cdot 10^5) n(2n2105) 个点和 n − 1 ≤ m ≤ min ⁡ ( 2 ⋅ 1 0 5 , n ( n − 1 ) 2 ) n-1 \le m \le \min(2 \cdot 10^5,\frac{n(n-1)}{2}) n1mmin(2105,2n(n1)) 条边的无向图,保证没有自环和重边。

现在需要你构造一棵生成树,使得 1 1 1 点的度数恰好为 D ( 1 ≤ D < n ) D(1 \le D < n) D(1D<n),或者说明无解。

8.2 解题过程

首先,如果原图中 1 1 1 点的度数小于 D D D 的话,一定无解。

我们将 1 1 1 暂时抠掉,去跑连通块,记录每一个连通块与 1 1 1 连边的点的个数以及这些点的集合。

如果连通块的个数大于 D D D,证明如果 1 1 1 的度数为 D D D 的话,无法使图联通。

否则,我们现将每个连通块中的一个点与 1 1 1 连边,如果 1 1 1 的度数不够 D D D 的话,再随便连接其他涉及到 1 1 1 的边。上述连边过程需要使用并查集维护联通情况。

最后按照传统的生成树思路,利用并查集连接剩下的边。

时间复杂度: O ( n α ( n ) ) O(n\alpha(n)) O(nα(n))

8.3 代码

int head[maxn], father[maxn], n, cnt, belong[maxn], tot, deg[maxn], perm[maxn], rankk[maxn];
bool vis[maxn];
vector <int> conn[maxn];
bool cmp(int a, int b)
{
    return deg[a] < deg[b];
}
struct edge
{
    int u, v, nxt;
} Edge[2 * maxn];
void init()
{
    memset(head, -1, sizeof(head));
    memset(belong, 0, sizeof(belong));
    for(int i = 0; i <= n; i++) father[i] = i;
    cnt = 0, tot = 0;
}
int query(int id)
{
    return father[id] == id ? id : query(father[id]);
}
void merge(int x, int y)
{
    x = query(x);
    y = query(y);
    if(x != y)
    {
        if(rankk[x] > rankk[y]) father[y] = x;
        else
        {
            father[x] = y;
            if(rankk[x] == rankk[y])    rankk[y]++;
        }
    }
}
void addedge(int u, int v)
{
    Edge[cnt].u = u;
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void dfs(int id)
{
    if(vis[id]) return;
    vis[id] = true;
    belong[id] = tot;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == 1)
        {
            deg[tot]++;
            conn[tot].pb(id);
            continue;
        }
        dfs(v);
    }
}
int main()
{
    int m, u, v, d;
    scanf("%d%d%d", &n, &m, &d);
    init();
    int degone = 0;
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        addedge(v, u);
        degone += (u == 1 || v == 1);
    }
    for(int i = 2; i <= n; i++)
    {
        if(!vis[i])
        {
            tot++;
            dfs(i);
        }
    }
    if(tot > d || degone < d)
    {
        printf("NO\n");
        return 0;
    }
    for(int i = 1; i <= tot; i++)   perm[i] = i;
    int num = 0;
    printf("YES\n");
    for(int i = 1; i <= tot; i++)
    {
        int id = perm[i];
        num++;
        printf("%d %d\n", 1, conn[id][0]);
        merge(1, conn[id][0]);
        if(num == d)    break;
    }
    for(int i = 1; i <= tot; i++)
    {
        int id = perm[i];
        for(int j = 1; j < conn[id].size(); j++)
        {
            if(num >= d)    break;
            num++;
            printf("%d %d\n", 1, conn[id][j]);
            merge(1, conn[id][j]);
        }
        if(num >= d)    break;
    }
    for(int i = 1; i <= cnt; i++)
    {
        u = Edge[i].u;
        v = Edge[i].v;
        if(u == 1 || v == 1)    continue;
        if(query(u) != query(v))
        {
            printf("%d %d\n", u, v);
            merge(u, v);
        }
    }
    return 0;
}

9. 723F - st-Spanning Tree(并查集 + 贪心)

题目链接:https://codeforces.com/contest/723/problem/F

9.1 题意

给你一个 n ( 2 ≤ n ≤ 2 ⋅ 1 0 5 ) n(2 \le n \le 2 \cdot 10^5) n(2n2105) 个点和 m ( 1 ≤ m ≤ min ⁡ ( 4 ⋅ 1 0 5 , n ( n − 1 ) 2 ) ) m(1 \le m \le \min(4 \cdot 10^5, \frac{n(n-1)}{2})) m(1mmin(4105,2n(n1))) 条边的无向连通图,保证没有自环和重边。

现在需要你构造一棵生成树,使得 s s s 的度数不超过 d s d_s ds t t t 的度数不超过 d t d_t dt,或者说明答案不存在。

9.2 解题过程

这类题的思路一般都是将 s s s t t t 暂时抠掉,然后跑连通块。

因为原图连通,所以所有的不连通都是 s s s t t t 造成的。

遍历所有的边,分几种情况:

  1. 连接 s s s t t t 的边:直接标记这条边是存在的(后面会用到),然后直接跳过。直接连接的话显然不划算(不如借助一个连通块)。
  2. 连接 s s s 和一个连通块:如果这个连通块没有记录过连接到 s s s 的边,就标记上,否则跳过;
  3. 连接 t t t 和一个连通块:与 2 同理。
  4. 其他类型的边,直接连边(用并查集合并,并将这条边存入答案中)。

之后枚举所有的连通块,又分几种情况:

  1. 这个连通块只连接 s s s,则将这个连通块标记为连接 s s s,并将 d s d_s ds 1 1 1
  2. 这个连通块只连接 t t t,则将这个连通块标记为连接 t t t,并将 d t d_t dt 1 1 1
  3. 这个连通块同时连接 s s s t t t,如果 s s s t t t 还未连通,则将这个连通块标记为同时连通 s s s t t t,并将 d s d_s ds d t d_t dt 同时减 1 1 1;否则如果 d s > 0 d_s > 0 ds>0,则将这个连通块标记为连接 s s s,并将 d s d_s ds 1 1 1,否则如果 d t > 0 d_t>0 dt>0,则将这个连通块标记为连接 t t t,并将 d t d_t dt 1 1 1

在这个过程中,如果发现 d s d_s ds d t d_t dt 小于 0 0 0,则直接输出无解。

之后再次枚举连通块,这一次就是按照之前规定的方案进行连边(用并查集合并,并将这条边存入答案中)了。

最后,如果发现 s s s t t t 仍然不连通,就需要连接 s s s t t t 了,但如果发现这条边并不存在,则直接输出无解。

这之后,还需要判断是否恰好连接了 n − 1 n-1 n1 条边,如果不是,输出无解。

否则,输出全部的连边即可。

时间复杂度: O ( n α ( n ) ) O(n \alpha (n)) O(nα(n))

9.3 代码

int n, m, cnt;
int s, t, ds, dt;
int head[maxn], col[maxn], father[maxn], rank_[maxn];
bool vis[2][maxn];
vector<int> stat[maxn];
vector<pii> ans;
int point[2][maxn];
int ist[maxn];
struct edge
{
    int v, nxt;
} Edge[4 * maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1, father[i] = i;
    cnt = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
int findfather(int id)
{
    return father[id] == id ? id : findfather(father[id]);
}
void merge(int x, int y)
{
    ans.pb(pii(x, y));
    x = findfather(x);
    y = findfather(y);
    if(x != y)
    {
        if(rank_[x] > rank_[y]) father[y] = x;
        else
        {
            father[x] = y;
            if(rank_[x] == rank_[y])    rank_[y]++;
        }
    }
}
void dfs(int id, int c)
{
    col[id] = c;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == s || v == t)    continue;
        if(col[v])  continue;
        dfs(v, c);
    }
}
int main()
{
    int u, v;
    scanf("%d%d", &n, &m);
    init();
    bool mark = false;
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        addedge(v, u);
    }
    scanf("%d%d%d%d", &s, &t, &ds, &dt);
    int numc = 0;
    for(int i = 1; i <= n; i++)
    {
        if(i == s || i == t)    continue;
        if(!col[i]) dfs(i, ++numc);
    }
    for(int i = 0; i < cnt; i += 2)
    {
        u = Edge[i ^ 1].v;
        v = Edge[i].v;
        if (u == s && v == t) mark = true;
        if (u == t && v == s) mark = true;
        if(findfather(u) == findfather(v))  continue;
        if((!col[u]) && (!col[v]))  continue;
        if(col[u] == col[v])    merge(u, v);
        else
        {
            if(!col[v]) swap(u, v);
            if(u == s)
            {
                if(!vis[0][col[v]])
                {
                    stat[col[v]].pb(u);
                    vis[0][col[v]] = true;
                    point[0][col[v]] = v;
                }
            }
            else
            {
                if(!vis[1][col[v]])
                {
                    stat[col[v]].pb(u);
                    vis[1][col[v]] = true;
                    point[1][col[v]] = v;
                }
            }
        }
    }
    for(int i = 1; i <= numc; i++)
    {
        if(stat[i].size() == 1)
        {
            int id = stat[i][0];
            if(id == s) ds--;
            else
            {
                dt--;
                ist[i] = true;
            }
        }
        if(ds < 0 || dt < 0)
        {
            printf("No\n");
            return 0;
        }
    }
    bool iscon = false;
    for(int i = 1; i <= numc; i++)
    {
        if(stat[i].size() == 2)
        {
            if(!iscon)
            {
                iscon = true;
                ds--;
                dt--;
                ist[i] = -1;
            }
            else
            {
                if(ds > 0)  ds--;
                else
                {
                    dt--;
                    ist[i] = true;
                }
            }
        }
        if(ds < 0 || dt < 0)
        {
            printf("No\n");
            return 0;
        }
    }
    for(int i = 1; i <= numc; i++)
    {
        if(ist[i] == 0) merge(s, point[0][i]);
        else if(ist[i] == 1) merge(t, point[1][i]);
        else
        {
            merge(s, point[0][i]);
            merge(t, point[1][i]);
        }
    }
    if(findfather(s) != findfather(t))
    {
        if (!mark) return 0 * puts("No");
        merge(s, t);
        ds--;
        dt--;
    }
    if(ds < 0 || dt < 0)
    {
        printf("No\n");
        return 0;
    }
    if(ans.size() < n - 1)  printf("No\n");
    else
    {
        printf("Yes\n");
        for(int i = 0; i < ans.size(); i++)
        {
            printf("%d %d\n", ans[i].first, ans[i].second);
        }
    }
    return 0;
}

10. 260D - Black and White Tree(“二分树” + 贪心)

题目链接:https://codeforces.com/problemset/problem/260/D

10.1 题意

给出每一个点 i i i 的颜色 c i ( c i ∈ [ 0 , 1 ] ) c_i(c_i \in [0,1]) ci(ci[0,1]),现在需要你构造一棵 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 个节点的树,并满足以下条件:

  1. 同色点不可相邻;
  2. 对于点 i i i 来说,其邻接边的权值总和恰好为 s i s_i si

题目保证答案一定存在。

10.2 解题过程

首先你要有一个这样的意识:一条权值为 w w w 的边 ( u , v ) (u,v) (u,v) 会将 s u s_u su s v s_v sv 都减去 w w w

将不同颜色的点放入不同的桶中,对每一个桶按照 s s s 值从大到小进行排序。

之后枚举颜色为 0 0 0 的点 u u u,我们的目的是贪心地用颜色为 1 1 1 的点 v v v s v s_v sv 来凑 u u u s u s_u su

因此我们用循环之外的 p o s pos pos 记录当前正在访问的颜色为 1 1 1 的点 v v v 在桶 1 1 1 中的位置,然后利用 p o s pos pos 枚举点 v v v,分两种情况:

  1. 如果 s u ≥ s v s_u \ge s_v susv,则直接将 u u u v v v 连边(并查集合并,并存储答案),权值为 s v s_v sv,之后将 s u s_u su 减去 s v s_v sv,这相当于消耗了 v v v 的所有 s v s_v sv u u u 连边,之后 u u u 还剩下了一些 s u s_u su 值没有使用,因此 u u u 不变,开始枚举下一个 v v v
  2. 如果 s u < s v s_u < s_v su<sv,这表明需要消耗 u u u 的所有 s u s_u su v v v 连边,之后 v v v 还剩下了一些 s v s_v sv 值没有使用,因此我们仍然将 u u u v v v 连边(并查集合并,并存储答案),但此时的权值为 s u s_u su,之后将 s v s_v sv 减去 s u s_u su,此时 v v v 不变,开始枚举下一个 u u u

这波操作完成之后,我们已经达到了题中所给的两个条件,但是整个图可能还是不连通,这时我们就要利用一下权值为 0 0 0 的边了。

这时我们需要利用颜色为 0 0 0 的第一个点与颜色为 1 1 1 的未联通的点连接权值为 0 0 0 的边。同理我们需要利用颜色为 1 1 1 的第一个点与颜色为 0 0 0 的未联通的点连接权值为 0 0 0 的边。这个过程仍然利用并查集维护,并在建边的过程中存储答案。

最终输出连边方案即可。

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn)

10.3 代码

int n;
vector<pii> ve[2];
bool cmp(pii a, pii b)
{
    return a.second > b.second;
}
struct edge
{
    int u, v, w;
    edge (int _u, int _v, int _w):
        u(_u), v(_v), w(_w) {}
};
vector<edge> ans;
bool vis[maxn];
int father[maxn], rank_[maxn];
void init()
{
    for(int i = 0; i <= n; i++) father[i] = i;
}
int findfather(int id)
{
    return father[id] == id ? id : father[id] = findfather(father[id]);
}
void merge(int a, int b)
{
    a = findfather(a);
    b = findfather(b);
    if(a != b)
    {
        if(rank_[a] > rank_[b]) father[b] = a;
        else
        {
            father[a] = b;
            if(rank_[a] == rank_[b])    rank_[b]++;
        }
    }
}
int main()
{
    int type, w;
    scanf("%d", &n);
    init();
    for(int i = 1; i <= n; i++)
    {
        scanf("%d%d", &type, &w);
        ve[type].pb(pii(i, w));
    }
    for(int i = 0; i < 2; i++)
    {
        sort(ve[i].begin(), ve[i].end(), cmp);
    }
    int pos = 0;
    for(int i = 0; i < ve[0].size(); i++)
    {
        int val = ve[0][i].second;
        vis[ve[0][i].first] = true;
        while(val)
        {
            if(ve[1][pos].second <= val)
            {
                vis[ve[1][pos].first] = true;
                val -= ve[1][pos].second;
                ans.pb(edge(ve[0][i].first, ve[1][pos].first, ve[1][pos].second));
                merge(ve[0][i].first, ve[1][pos].first);
                ve[1][pos].second = 0;
                pos++;
            }
            else
            {
                vis[ve[1][pos].first] = true;
                ans.pb(edge(ve[0][i].first, ve[1][pos].first, val));
                merge(ve[0][i].first, ve[1][pos].first);
                ve[1][pos].second -= val;
                val = 0;
            }
        }
    }
    for(int i = 0; i < ve[1].size(); i++)
    {
        if(findfather(ve[0][0].first) != findfather(ve[1][i].first))
        {
            merge(ve[0][0].first, ve[1][i].first);
            ans.pb(edge(ve[0][0].first, ve[1][i].first, 0));
        }
    }
    for(int i = 0; i < ve[0].size(); i++)
    {
        if(findfather(ve[1][0].first) != findfather(ve[0][i].first))
        {
            merge(ve[1][0].first, ve[0][i].first);
            ans.pb(edge(ve[1][0].first, ve[0][i].first, 0));
        }
    }
    for(int i = 0; i < n - 1; i++)
    {
        printf("%d %d %d\n", ans[i].u, ans[i].v, ans[i].w);
    }
    return 0;
}

你可能感兴趣的:(Codeforces,图论)