• 【算法每日一练]-图论(保姆级教程 篇2(topo排序,并查集,逆元))#topo排序 #最大食物链 #游走 #村村通


    今天讲topo排序

    目录

    题目:topo排序

    思路:

    题目:最大食物链

    解法一:

    解法二: 记忆化

    题目:村村通

    思路:


             

    前言:topo排序专门处理DAG(有向无环图)

    题目: topo排序

    :你有n本书(1~n),阅读第i本数前你要先读Ci本书,现在你要阅读第一本书,问需要阅读那些书?(答案不唯一)

           

    思路:

         

    看到这样想遍历下一个节点就需要把所有前置都先遍历过的特点,topo就行了。

    先把没有前置的书看一下,然后把后置书的前置书数减一,然后看下一个能看的书。

          

    主要就是标记维护一个in数组(存入每个点的前置书数)

          

    1. #include
    2. using namespace std;
    3. const int N=2e5+5;
    4. vector<int>ve[N];
    5. vector<int>ans;
    6. int n,in[N],vis[N];
    7. void dfs(){//dfs把阅读1书的所有前置书都遍历标记出来,标记出一种方案即可(因为topo序不唯一)
    8. queue<int>q;
    9. q.push(1);
    10. while(!q.empty()){
    11. int cur=q.front();q.pop();
    12. for(int i=0,sz=ve[cur].size();i
    13. int u=ve[cur][i];
    14. if(!vis[u])q.push(u);vis[u]=1;
    15. }
    16. }
    17. }
    18. void topo(){//正序topo排序
    19. queue<int>q2;
    20. for(int i=1;i<=n;i++){//先找出入度为0的点。 当然你完全可以写q.push(1),而我们这里只是为了提供一个topo模板
    21. if(in[i]==0)q2.push(i),ans.push_back(i);
    22. }
    23. while(!q2.empty()){
    24. int cur=q2.front();q2.pop();
    25. for(int i=0,sz=ve[cur].size();i
    26. int u=ve[cur][i];in[u]--;//去掉一个点就减掉相邻点的入度
    27. if(in[u]==0)q2.push(u),ans.push_back(u);//入队的点都是等待去掉的点
    28. }
    29. }
    30. }
    31. int main(){
    32. cin>>n;
    33. for(int i=1;i<=n;i++){//输入每本书需要的前置书Ci
    34. int cnt,u;cin>>cnt;
    35. for(int j=1;j<=cnt;j++){
    36. cin>>u;//每本前置书编号
    37. ve[i].push_back(u);in[u]++;
    38. }
    39. }
    40. dfs();//标记1的前置书
    41. topo();//topo排序遍历出需要的书,我们倒序输出即可
    42. for(int i=ans.size()-1;i>=0;i--){
    43. if(vis[ans[i]]) cout<' ';
    44. }
    45. }

               

                

    题目:最大食物链

             

    解法一:

          

    我们标记f[i]是被f[x]捕食的点对应的类食物链数

    不难得出: f[x]=∑(f[i])   
    首先从生产者开始,每去掉一个被捕食的点,那么相邻捕食者就要加上去掉点的类食物链数,但是我们还需要找到出度为0的消费者。
    所以这道题,我们要同时记录入度,还有出度(其实单纯的topo排序就用不上出度,记录出度是为了找食物链结尾的个数)

          

    1. #include
    2. using namespace std;
    3. const int MOD=80112002,M=500005,N=5005;
    4. vector <int>v[N];
    5. queue<int> q;
    6. int n,m,ans,f[N],in[N],out[N];//我们需要标记每个点的入度和出度,f为以该点结尾的类食物链数
    7. int main(){
    8. cin>>n>>m;int x,y;
    9. for(int i=1;i<=m;i++){
    10. cin>>x>>y;
    11. v[x].push_back(y);//y吃x,x指向y
    12. out[x]++;in[y]++;
    13. }
    14. for(int i=1;i<=n;i++){//找到所有没有入度的点为起点
    15. if(in[i]==0){q.push(i);f[i]=1;}
    16. }
    17. while(q.size()){//进行拓扑排序
    18. int cur=q.front();q.pop();
    19. for(int i=0,sz=v[cur].size();i
    20. int t=v[cur][i];
    21. f[t]=(f[t]+f[cur])%MOD;in[t]--;//去掉cur点的话,就要把f[cur]加到捕食它的点上
    22. if(in[t]==0) q.push(t);
    23. }
    24. }
    25. for(int i=1;i<=n;i++){
    26. if(out[i]==0)ans=(ans+f[i])%MOD;//出度为0的点的f是我们要的真正食物链数
    27. }
    28. cout<
    29. return 0;
    30. }

               

    解法二: 记忆化

    1. #include //记忆化搜索
    2. using namespace std;
    3. const int N=1e5+5;
    4. int n,m,ans,tot,in[N],out[N],f[N];
    5. vector<int>ve[N];
    6. int dfs(int x){//就是从每个生产者开始,看看能到多少个最终消费者,然后记忆化,最终计算所有生产者就是答案
    7. if(f[x])return f[x];
    8. if(!out[x])return 1;
    9. for(int i=0,sz=ve[x].size();i
    10. int v=ve[x][i];
    11. f[x]+=dfs(v);
    12. }
    13. return f[x];
    14. }
    15. int main(){
    16. cin>>n>>m;int u,v,w;
    17. while(m--){
    18. cin>>u>>v;
    19. ve[u].push_back(v);
    20. out[u]++;in[v]++;
    21. }
    22. for(int i=1;i<=n;i++){
    23. if(in[i]==0&&out[i])
    24. ans+=dfs(i);
    25. }
    26. cout<
    27. }

             

            

    题目:游走

             

    思路: 

           

    给一个DAG(有向无环图),求所走路径长度的期望呗!也就是:所有路径长度总和/所有路径个数(因为每条路径概率都一样嘛)

            

    明明是DAG图,topo一下完事了

    我们设置g[i]表示以i为终点的路径数,f[i]表示i为终点的长度和

       
    topo:从点j到一个点i,则g[i]+=g[j],f[i]+=f[j]+g[i](因为啊,从j到i的每个路径长度都只增加1就行,一共增加了g[i])

            
    最后就是求(L/S)MOD,也就是L*(S^(MOD-2))MOD即可(逆元小知识)

            

    1. #include
    2. using namespace std;
    3. typedef long long ll;
    4. const int MAXN=1e5+5;
    5. const ll MOD=998244353;
    6. int n,m;
    7. ll L,S,f[MAXN],g[MAXN];//g[i]表示以i为终点的路径数,f[i]表示i为终点的长度和
    8. vector<int> edge[MAXN];
    9. int in[MAXN],vis[MAXN];
    10. void topo(void)//模板
    11. {
    12. queue<int> q;
    13. for(int i=1;i<=n;i++)
    14. if(!in[i]) q.push(i);
    15. while(!q.empty())
    16. {
    17. const int u=q.front();
    18. q.pop();
    19. if(vis[u]) continue; vis[u]=true;//没有环,所以这句话可以不要
    20. for(auto v:edge[u])
    21. {
    22. in[v]--;
    23. if(!in[v]) q.push(v);
    24. f[v]=(f[v]+f[u]+g[u])%MOD;
    25. g[v]=(g[v]+g[u])%MOD;
    26. }
    27. }
    28. }
    29. ll qpow(ll base,ll k)//快速幂求逆元
    30. {
    31. ll res=1;
    32. while(k)
    33. {
    34. if(k&1) res=res*base%MOD;
    35. base=base*base%MOD;
    36. k>>=1;
    37. }
    38. return res;
    39. }
    40. int main()
    41. {
    42. cin>>n>>m;
    43. for(int i=1;i<=m;i++)
    44. {
    45. int u,v;cin>>u>>v;
    46. edge[u].push_back(v);
    47. in[v]++;
    48. }
    49. for(int i=1;i<=n;i++) g[i]=1;//初始化:每个点的路径数初始化为1
    50. topo();
    51. for(int i=1;i<=n;i++) L=(L+f[i])%MOD;//获取最终的总长度
    52. for(int i=1;i<=n;i++) S=(S+g[i])%MOD;//获取最终的路径个数
    53. cout<<(L*qpow(S,MOD-2))%MOD<//求L/S的结果,即L*S的逆元,即L*S^(MOD-2)
    54. return 0;
    55. }

    提一嘴: 

                        
    为什么要引入逆元呢?
    因为(a+b)%MOD=(a%MOD+b%MOD)%MOD
    (a-b)%MOD=(a%MOD-b%MOD)%MOD
    (a*b)%MOD=(a%MOD*b%MOD)%MOD  

                
    但是除法不满足,我们要求(a/b)%p=1等价于(a*x)%p=1,这个x就是b的乘法逆元(可以理解成x为1/b),也就是(b*x)%p=1

            
    再引入费马小定理:假如a和p互质,那么a^(p-1)=1(%p),故a*a^(p-2)=1(%p),故a的逆元x=a^(p-2)%p
    因此:(x/y)%p等价于x*y^(p-2)%p,注意每乘一次就要去一次模
         

            

                     

    题目:村村通

     

                

    思路:

           

    并查集步骤:
    1,初始化每个点的父亲为自身
    2,查找每两个元素所在的集合,找到两个祖宗后返回任意一个并路径压缩(修改中间点的父亲编号为祖宗编号)
    3,最后查找有几个祖宗即可(因为有相同的祖宗,就说明祖宗可以到的所有点,也就是它们都是互通的)
          

    1. #include
    2. using namespace std;
    3. int fa[1000001], n, m, x, y;
    4. int find(int x)//找到祖先,并将中间的点的父亲都修改成祖宗编号(路径压缩)
    5. {
    6. if(x!=fa[x])//当x不等于它的爸爸时(还有祖先)
    7. fa[x]=find(fa[x]);//找到祖先,并修改父亲
    8. return fa[x];//返回祖先
    9. }
    10. void unity(int x, int y)
    11. {
    12. int f1=find(x);//找到x的祖先 f1
    13. int f2=find(y);//找到y的祖先 f2
    14. fa[f1]=f2;//祖先和祖先结为父子(谁是父亲谁是儿子都可以)
    15. }
    16. int main()
    17. {
    18. while(true)
    19. {
    20. int ans=0;
    21. cin>>n>>m;
    22. if(n==0) return 0;
    23. for(int i=1; i<=n; i++){
    24. fa[i]=i;//初始化自己的父亲是自己
    25. }
    26. for(int i=1; i<=m; i++){
    27. scanf("%d %d", &x, &y);//一点点把图连通起来
    28. unity(x,y);//连通:合并x和y各自能到的地方
    29. }
    30. for(int i=1; i<=n; i++){//自己的父亲等于自己本身
    31. if(find(i)==i) ans++;
    32. }
    33. printf("%d\n", ans-1);//共需修ans-1条路即可
    34. }
    35. return 0;
    36. }

  • 相关阅读:
    RabbitMQ高级特性
    C语言之ASC转hex
    什么是迭代器,Python迭代器及其用法
    同城多数据中心部署 TiDB
    angular:html2canvas报错提示Unable to find iframe window
    【代码随想录】算法训练营 第一天 第一章 数组 Part 1
    ceph 14.2.10 aarch64 非集群内 客户端 挂载块设备
    HiveSQL和SparkSQL的区别和联系
    推荐算法:是否对用户判断能力有影响!!!
    数据结构初阶--双向循环链表(讲解+类模板实现)
  • 原文地址:https://blog.csdn.net/m0_69478376/article/details/134357427