草庐IT

「并查集」改变道路

孤星·璀璨 2023-03-28 原文

本题为1月1日22寒假集训每日一题题解

题目来源:(未知)

题面

题目描述

有n个城市和m条道路,每条道路连接两个城市。每条道路都是单向的,但你可以决定每条道路的方向。一个城市如果没有通向它的道路,就被认为是独立的,但允许有从这个城市通往其他城市的道路。请问最小的独立城市的数量。

输入

第一行输入两个数字n,m。表示城市的数量和道路的数量。接下来m行每行两个数字u,v,表示u,v两座城市中有一条道路相连。(2 ≤ n ≤ 100 000, 1 ≤ m ≤ 100 000).

输出

输出一个数字,表示独立城市的最小数量。

样例输入

【样例输入1】
4 3
2 1
1 3
4 3
【样例输入2】
5 5
2 1
1 3
2 3
2 5
4 3
【样例输入3】
6 5
1 2
2 3
4 5
4 6
5 6

样例输出

【样例输出1】
1
【样例输出2】
0
【样例输出3】
1

思路分析

本题需要较多的思考,对思维要求比较大,希望各位阅读时多多思考.

本题咋一看难以入手,甚至毫无头绪.为了找到思路,所以我们从最简单的情况入手来看,逐步分析.

本题所形成的结构显然是,而最简单的显然是,所有点成线性的情况.对于这种情况,想要使得最终独立的点最少,显然只需要每条边向着同一个方向即可.此时只有位于开头的点是独立点,且显然我们无法产生0个独立点的情况,故此情况确实取得最少独立点.

接下来是形成的情况.也有很多情况,所以我们从最简单的情况分心.首先,显然,对于一颗倒V形的树,其实就是把线性表中间的某个节点折了过来,此时与线性表的情况是一致的,其中一个叶子节点作为开头的独立点,每条边指向一个其他节点即可.

那如果是倒W形的树呢?此时可以看成是将一条线性表接在了另一个线性表的中间某个节点上,然后把这个节点折了过来,所以此时与线性表依然是一致的,其中一个叶子节点作为开头的独立点,每条边指向一个其他节点即可.

用同样的理解方法,对于一颗任意的树,我们都可以看成若干线性表的组合,那么同样,只需要一个叶子结点作为开头的独立点,每条边指向一个其他节点,即可取得最小独立点数的情况.

当然,如果认真观察,我们会发现此时我们又另外一种指定边方向的方案,即将根节点的入度看成0,其他边均指向子节点,此时同样只有一个独立点(根节点).其实再仔细观察我们也可以发现,此规律和前面线性表得出的规律也是符合的(我们可以把线性表的第一个节点看成根节点).为了后续叙述方便,此处我们采用这种规律,而不是上面把叶子结点作为独立点的规律.

接着就是的情况了.在连通(不是强连通,下同)的无环图中,显然可以和树一样去理解,找出合适的根节点,将其作为独立点即可.此时最小的独立点数依旧是1,且显然无法取得0个独立点的情况.

但是对于连通的有环图来说,上面的规律就不适用了,不过好在我们可以近似去理解.我们可以把环中的一个点看成"根节点",由于存在环,此时我们只需要将形成环的那条边调转方向,指向这个"根节点",即可使得这个根节点不再独立,此时整个图上不会存在任何一个独立点(可以看成,加上这条形成环的边前,此图只有"根节点"是独立点;在加上这条边后,由于其方向指向"根节点",所以"根节点"不再独立),此时最小独立点数为0.

综上,我们成功分析了连通图可能出现的所有情况,总结起来就是,无环最小独立点数为1,有环为0.

那么对于不连通的图呢?显然,可以当成多个子图来处理,每一个无环子图能产生一个最小独立点数,每个有环子图能产生0个最小独立点数.所以,我们只需要统计所有子图中的无环图即可.

到此这道题的思路就有了,我们只需要根据输入进来的数据,判断有多少个子图是无环图即可.无环子图的数目就是最终要求的最小独立点数.我们有多种方法取得答案,这里数据量比较大,且由于只需要看连通性和有无环,所以我使用了时间复杂度接近常数(实际为阿克曼函数的反函数)的并查集来处理.当然也有其他做法,比如直接用深度优先搜索,这样也可以找出有几个无环的子图,不过时间复杂度劣于并查集的做法,感兴趣可以自行研究(很好写).

并查集可以动态维护出每个连通块,利用它即可得到当前图子图的数量.而当将加入新的一条边时,如果并查集中已经记录这条边所连接的两个点属于同一个连通块,那么这条边的加入就会使得这幅子图中形成环.所以我们只需要动态维护一个计数器,初始值为这个图中点的个数(在连接所有边之前,一个n个顶点的图可以看成是由n个子图组成的图,每个子图就是一个点),在动态连接每条边时,如果两个图合并,其中一个为无环图,计数器自减;如果一个图成为了有环图,计数器自减.这样即可在所有点输入之后直接取得我们所需要的答案.

参考代码

由于存在并查集,此题的代码较长,这里为了阅读方便,将并查集和核心代码拆开成两部分

并查集部分(使用面向对象的编程思想,此处代码似乎存在bug,但是交上去是AC的):

namespace std
{
    /*
        简易并查集
        使用不可变数组实现,各元素编号从0开始
        为方便扩展,对外开放内部使用的两个数组

        此题中为了实现算法,增加了记录每个连通块是否有环的数组和记录独立城市数量变量两个属性

        @author 星·双子
        @version 1.0
        @date 2022/10/29
    */
    class UnionFindSets
    {
    private:
    public:
        /*
            模拟链表
        */
        int *par;
        /*
            记录每个连通部分路径压缩前树的深度
        */
        int *rank;
        /*
            此题额外部分,记录每个连通块是否有环
        */
        bool *isLoop;
        /*
            此题额外部分,记录独立城市数量(独立城市即为无环的每组的一个叶子节点)
        */
        int count;

        UnionFindSets(int);
        ~UnionFindSets();
        int find(int);
        void unite(int, int);
        bool same(int, int);

        int operator[](int);
    };

    /*
        查询指定元素的根节点

        均摊时间复杂度O(α(n))

        @param x{int} 指定的元素编号
        @return {int} 根节点元素编号
    */
    int UnionFindSets::find(int x)
    {
        if (par[x] == x) // 基线条件,本身就是根节点,直接返回
            return x;

        // 不断向上递归地搜索根节点,同时进行路径压缩,将当前结点的父节点直接改成根节点
        return par[x] = find(par[x]);
    }

    /*
        将[]重载为find函数,通过[]运算符即可获取根节点
        但不支持通过此方式来连通两个连通部分

        @return {int} 根节点元素编号
    */
    int UnionFindSets::operator[](int x)
    {
        return find(x);
    }

    /*
        连通两个连通部分

        均摊时间复杂度O(α(n))

        @param x{int} 第一个连通部分中的任意一个元素编号
        @param x{int} 第二个连通部分中的任意一个元素编号
    */
    void UnionFindSets::unite(int x, int y)
    {
        // 分别找到两个连通部分的根节点
        x = find(x);
        y = find(y);

        // 如果本来就连通,再次进行连通即会成为环
        if (x == y)
        {
            // 如果之前没有成环,由于可以将环中的一个点看成新的顶点,并令其两边一进一出来消除一个独立点,所以需要把独立点数量减一
            if (!isLoop[x])
            {
                count--;
                isLoop[x] = true; // 标记此连通块存在环
            }

            return;
        }

        // 将深度小的连通部分接在大的上
        if (rank[x] < rank[y])
        {
            // 如果之前没有成环,由于少了一个顶点(被合并进了新的连通块),相当于少了一个独立点,把计数器减一
            if (!isLoop[x])
            {
                count--;
            }
            isLoop[x] |= isLoop[y]; // 同步连通状态

            par[x] = y;
        }
        else
        {
            // 如果之前没有成环,由于少了一个顶点(被合并进了新的连通块),相当于少了一个独立点,把计数器减一
            if (!isLoop[y])
            {
                count--;
            }
            isLoop[y] |= isLoop[x];// 同步连通状态

            par[y] = x;
            // 如果两个连通部分深度一样,此时连通后深度会加一
            if (rank[x] == rank[y])
            {
                rank[x]++; // 只需要操作根节点就好了
            }
        }
    }

    /*
        判断两个元素是否连通

        @param x{int} 元素1
        @param x{int} 元素2
        @return {bool} 是否连通
    */
    bool UnionFindSets::same(int x, int y)
    {
        // 判断根节点是否相同即可
        return find(x) == find(y);
    }

    /*
        构造函数
        初始化相关数组

        时间复杂度O(n)

        @param {int} 总结点数
    */
    UnionFindSets::UnionFindSets(int n)
    {
        par = new int[n];
        rank = new int[n];
        isLoop = new bool[n];
        count = n;
        for (int i = 0; i < n; i++)
        {
            par[i] = i;
            rank[i] = 0;
            isLoop[i] = false;
        }
    }

    /*
        析构函数
        释放相关内存空间
    */
    UnionFindSets::~UnionFindSets()
    {
        delete par;
        par = nullptr;
        delete rank;
        rank = nullptr;
    }
}

核心代码部分:

#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <iostream>

using namespace std;

int main()
{
    ios::sync_with_stdio(false);

    int n, m;
    cin >> n >> m;
    UnionFindSets ufs(n); // 并查集对象

    while (m--)
    {
        int u, v;
        cin >> u >> v;
        u--; // 将编号转为从1开始
        v--; // 将编号转为从1开始

        ufs.unite(u, v); // 将两个节点连通
    }

    cout << ufs.count << "\n"; // 直接获取并查集中计算出的独立点数量即可,由于n额外加了1,所以这里需要把1减回来

    return 0;
}

"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德

有关「并查集」改变道路的更多相关文章

  1. ruby - 改变替换的大小写 - 2

    我有以下内容:text.gsub(/(lower)(upper)/,'\1\2')我可以将\2替换为大写吗?类似于:sed-e's/\(abc\)/\U\1/'这在Ruby中可行吗? 最佳答案 查看gsub文档:str.gsub(模式){|匹配|block}→new_str在block形式中,当前匹配字符串作为参数传入,$1、$2、$`、$&、$'等变量将被适当设置。block返回的值将替换为每次调用的匹配项。"alowerupperb".gsub(/(lower)(upper)/){|s|$1+""+$2.upcase}

  2. ruby - 为什么我不能改变 self 的值(value)? - 2

    为什么我可以这样改变“self”:self.map!{|x|x*2}或者这样:self.replace(self.map{|x|x*2})但不是这样:self=self.map{|x|x*2}为什么Ruby不允许我更改“self”变量指向的对象,但允许我更改对象的属性? 最佳答案 不是答案,只是一个线索。a=[1,2]=>[1,2]a.object_id=>2938a.map!{|x|x*2}=>[2,4]a.object_id#differentdatabutstillthesameobject=>2938a.replace(a.

  3. ruby - 为什么括号内的换行符会改变算术结果? - 2

    为什么下面的表达式会这样解析?括号的优先级应该高于换行符,不是吗?3-(1+1)#=>13-(1+1)#=>2省略加号也会让表达式计算为2:3-(11)#=>2如果我声明为连续的换行符(转义)或将加号移动到第一行,则会实现所需的行为:3-(1\+1)#=>13-(1+1)#=>1 最佳答案 这是因为Ruby将新行识别为表达式的结尾,除非表达式不完整。例如,(1+1)与相同(1;+1)这与+1相同,因为返回了括号内的最后一个表达式。这进一步与1相同。如果行尾有+,则表达式不完整,因此会继续到下一行。这使得:3-(1+1)被解释为3-(

  4. ruby-on-rails - 设计 Controller 如何改变布局? - 2

    这个问题在这里已经有了答案:differentlayoutforsign_inactionindevise(8个答案)关闭7年前。如何更改设计Controller中的布局?

  5. ruby-on-rails - Rails 3.2.13 与 Rails 4.0.1 - 改变了吗?方法变了? - 2

    我最近注意到ActiveRecord对象上的方法changed?在Rails3.2.13和Rails4.0.1之间发生了变化。问题在于连接到数据库中整数字段的字段。假设我的模型Model带有number整数字段:#Rails3.2.13m=Model.lastm.number#=>5m.number='5hello'm.number#=>5m.number_changed?#=>truem.changed?#=>truem.changes#=>{:number=>[5,5]}#Rails4.0.1m=Model.lastm.number#=>5m.number='5hello'm.nu

  6. ruby - 如何像 instance_eval 方法那样在一个 block 中改变 self 呢? - 2

    instance_eval方法在其block中改变自身,例如:classD;endd=D.newd.instance_evaldoputsself#printsomethinglike#,not'main'!end如果我们自己定义一个方法(或任何其他方法(除了instance_eval)需要一个block),当打印self时,我们将得到'main',这与instance_eval方法不同。例如:[1].eachdo|e|putsself#print'main'end我如何定义一个像instance_eval这样的方法(需要一个block)?提前致谢。 最佳答

  7. ruby-on-rails - friendly_id slug 在更新时没有改变 - 2

    我正在使用friendly_id5.0.0.rc1,还有active_admin。除了在active_admin中更新记录的slug属性/列没有做任何事情(它保持不变)之外,一切似乎都按预期完美运行我在使用控制台时发现了相同的行为:p=Post.firstp.slug#=>'test'p.slug='another-test'p.save#=>truep.slug#=>'test我的配置:FriendlyId.defaultsdo|config|config.use:reservedconfig.reserved_words=%w(adminneweditindexsessionuse

  8. ruby-on-rails - Rails 3 has_many 改变了吗? - 2

    我需要跟踪这样设置的关联的更改(添加和删除):has_many:listing_serviceshas_many:services,through::listing_services对于普通属性,最简单的方法是检查before_save或l.previous_changes[attribute]中的l.changes[attribute]>在after_save中。问题是,对于has_many属性,最好的方法是什么? 最佳答案 我没有使用changes方法。但我相信你总能使用魔法_changed?和_was:services.any

  9. ruby - 如何在 Ruby 中动态改变继承 - 2

    我想在Ruby中为一个类动态指定父类。考虑这段代码:classAgentdefself.hook_up(calling_class,desired_parent_class)#DosomemagichereendendclassParentdefbarputs"bar"endendclassChilddeffooputs"foo"endAgent.hook_up(self,Parent)endChild.new.barParent和Child类定义均未指定父类,因此它们都继承自Object。我的第一个问题是:我需要在Agent.hook_up中做什么才能使Parent成为Child的父

  10. ruby - 为什么 Ruby 中的双铲不改变状态? - 2

    我遇到了导致错误或困惑的奇怪副作用。所以想象一下,这不是一个微不足道的例子,而是一个陷阱的例子。name="Zorg"defsay_hello(name)greeting="Hithere,"这不会改变名称。姓名仍为Zorg.但是现在来看一个非常细微的差别。在下一个示例中:name="Zorg"defsay_hello(name)greeting=name现在名称是Zorg?.疯狂的。greeting=的细微差别|任务。Ruby在内部使用解析(?)或消息传递链接做一些事情?我以为这只会像name.一样把铲子链起来但我想这不会发生。这就是为什么我在尝试进行串联时避免使用铲子运算符。我通常

随机推荐