2.图论例题

1.图的遍历

1.生化危机

image-20201128163946151

image-20201128163959391

可用dfs或者bfs遍历即可:

#include <bits/stdc++.h>
using namespace std;
int main(){
    int m,n,k;
    cin>>m>>n>>k;
    map<int,vector<int>> st;
    for (int i = 0; i <m ; ++i) {
        st[i]=vector<int>{};
    }
    //安全城市
    vector<bool> isSafe(m, false);
    vector<bool> vis(m, false);
    for (int i = 0; i < n; ++i) {
        int temp;
        cin>>temp;
        isSafe[temp]= true;
    }
    //公路
    int a,b;
    for (int j = 0; j <k ; ++j) {
        cin>>a>>b;
        st[a].push_back(b);
        st[b].push_back(a);
    }
    //起止
    int start,end;
    cin>>start>>end;
    if(!isSafe[end]){
        cout<<"The city "<<end<<" is not safe!"<<endl;
        return 0;
    }
    //队列
    queue<int> q;
    q.push(start);
    while(!q.empty()){
        int top=q.front();
        q.pop();
        vis[top]= true;
        if(top==end){
            cout<<"The city "<<end<<" can arrive safely!"<<endl;
            return 0;
        }
        for (int i = 0; i < st[top].size() ; ++i) {
            int cur=st[top][i];
            if(!vis[cur]&& isSafe[cur]){
                q.push(cur);
            }
        }
    }
    cout<<"The city "<<end<<" can not arrive safely!";

}

2.节点间通路

image-20201128202933429

考察图的遍历:

//bfs版本:
bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
    unordered_map<int,vector<int>> st;
    vector<bool> vis(n+1, false);
    for( auto k: graph){
        st[k[0]].push_back(k[1]);
    }
    queue<int> qu;
    qu.push(start);
    while ( !qu.empty() ){
        int top=qu.front();
        qu.pop();
        vis[top]= true;
        if (top == target )return true;
        for (int i=0;i<st[top].size();i++){
            if( !vis[st[top][i]]){
                qu.push(st[top][i]);
            }
        }
    }
    return false;
}


//dfs版本:
map<int,vector<int>> st;
vector<bool > flag;
bool dfs(int cur,int target){
    if(cur==target){
       return true;
    }
    flag[cur]= true;
    for(int i=0;i<st[cur].size();i++){
        if(!flag[st[cur][i]]){
            if(dfs(st[cur][i],target))return true;
        }
    }
    return false;
}
bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
    flag=vector<bool>(n, false);
    for (int i = 0; i <n ; ++i) {
        st[i]=vector<int>();
    }
    for(auto k:graph){
        st[k[0]].push_back(k[1]);
    }
    return dfs(start,target);
}

2.最短路

1. 哈利·波特的考试

image-20201128164119865

输入样例

6 11
3 4 70
1 2 1
5 4 50
2 6 50
5 6 60
1 3 70
4 6 60
3 6 80
5 1 100
2 4 60
5 2 80

输出样例:

4 70

题目分析:

简单理解就是:

每个节点到其他节点的最短的路径中,最长的那一条构建一个集合

从这个集合中寻找最短的一跳,作为答案

这里用的是floyd最短路算法

#include <bits/stdc++.h>
using namespace std;
int inf=1000000;
//寻找节点V到其他节点的最长路径
int find(vector<vector<int>> graph,int v){
    int m=-100000;
    for (int i = 1; i <graph.size() ; ++i) {
        if( i!= v){
            m=max(m,graph[v][i]);
        }
    }
    return m;
}
int main(){
    int n,m;
    cin>>n>>m;
    vector<vector<int>> graph(n+1,vector<int>(n+1,inf));
    int a,b,value;
    for (int i = 0; i <m ; ++i) {
        cin>>a>>b>>value;
        graph[a][b]=value;
        graph[b][a]=value;
    }
    
    #floyd构建最短路
    for (int k = 1; k <=n ; ++k) {
        for (int i = 1; i <=n ; ++i) {
            for (int j = 1; j <=n ; ++j) {
                graph[i][j]=min(graph[i][j],graph[i][k]+graph[k][j]);
            }
        }
    }
    int ans=0;
    int maxLen=inf;
    #寻找答案
    for(int i=1;i<=n;i++){
        int cur=find(graph,i);
        if(cur<maxLen){
            maxLen=cur;
            ans=i;
        }
    }
    if( maxLen != inf){
        cout<<ans<<" "<<maxLen;
    }else{
        cout<<0;
    }
}

2. 被Gank的亚索

image-20201128202242616

输入示例

5 6 1 5
1 2 5
2 5 5
1 3 3
3 5 7
1 4 1
4 5 9

输出示例

10 3

题目分析:

题目想让我们找最短路径的数目,需要注意的是,两个节点间读取数据时相同长度的路径不止一条(想一想LOL的地图),因此应该事先记录

其次,因为两节点间的路径不止一条,所以在计算答案的时候,应用*,而不是+

堆优化版dijkstra最短路:

#include <bits/stdc++.h>
using namespace std;
int main(){
    int n,m,s,e;
    cin>>n>>m>>s>>e;
    int inf=0x3f3f3f3f;
    typedef pair<int,int> ll;
    //图
    vector<vector<int>> graph(n+1,vector<int>(n+1,0));
    //堆
    priority_queue<ll,vector<ll>,greater<ll>> st;
    //距离
    vector<int> dist(n+1,inf);
    //标志
    vector<bool> vis(n+1, false);
    //路径数量
    vector<int> lowestNums(n+1,1);
    vector<vector<int>> roadNums(n+1,vector<int>(n+1,0));
    int a,b,value;
    for (int i = 0; i <m ; ++i) {
        cin>>a>>b>>value;
        if ( ! graph[a][b] || value<graph[a][b] ){
            graph[a][b]=value;
            graph[b][a]=value;
            roadNums[a][b]=1;
            roadNums[b][a]=1;
        }else if ( value== graph[a][b] ){
            roadNums[a][b]++;
            roadNums[b][a]++;
        }
    }
    st.push({0,s});
    dist[s]=0;
    vis[s]= true;
    while ( !st.empty() ){
        ll cur= st.top();
        st.pop();
        vis[cur.second]= true;
        for( int i=1;i<=n;i++){
            if( graph[cur.second][i] && !vis[i] ){
                if( cur.first+graph[cur.second][i] <dist[i] ){
                    dist[i]= cur.first+graph[cur.second][i];
                    st.push({dist[i],i});
                    lowestNums[i] = lowestNums[cur.second]*roadNums[cur.second][i];
                }
                else if ( cur.first+graph[cur.second][i] == dist[i] ){
                    lowestNums[i] += lowestNums[cur.second]*roadNums[cur.second][i];
                }
            }
        }
    }
    cout<< dist[e]<<" "<<lowestNums[e];
}

3.并查集

1.合并账户

image-20201128205058574

并查集,问题在于,如何记录两个列表是否是同一个人的?

判断依据:有没有相同的邮箱

所以可以这样想

初始版本:

1.循环每一个列表的每一个邮箱,和别的列表比较,是否存在别的列表中

改进:

1.用一个set保存邮箱,如果邮箱没有出现过,就记录第一次出现时的父类

2.如果出现过,就合并两个父类

c++;

class Solution {
public:
  int graph[10000]={0};

int  find(int i){
    if(graph[i]==i)return graph[i];
    else return graph[i]=find(graph[i]);
}
void Union(int i,int j){
    int p1=find(i);
    int p2=find(j);
    if(p1!=p2){
        graph[p2]=p1;
    }
}
//用于保存该邮箱是否有出现过
set<string>isTurn;
//用于保存该邮箱的父类
map<string,int> emailFa;
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
     int n=accounts.size();
     for(int i=0;i<n;i++){
         graph[i]=i;
     }
     //1.遍历,合并
     for(int i=0;i<n;i++){
         for(int j=1;j<accounts[i].size();j++){
             if(isTurn.count(accounts[i][j])==0){
                 //第一次出现,则保存记录
                 isTurn.insert(accounts[i][j]);
                 emailFa[accounts[i][j]]=i;
             }else{
                 //否则,说明同一个人,合并
                   Union(emailFa[accounts[i][j]],i);
             }
         }
     }
     //2.将每个邮箱放到相对应的父类下面,sort可以自动排序
     map<int,set<string>> m;
     for(int i=0;i<n;i++){
        // cout<<graph[i]<<" ";
         int p=find(i);
          if(m.count(p)==0)m[p]=set<string>();
         for(int j=1;j<accounts[i].size();j++){
             m[p].insert(accounts[i][j]);
         }
     }
     //转化成相应的结构
    vector<vector<string>> result;
     for(auto acc:m){
         vector<string> ans;
         ans.push_back(accounts[acc.first][0]);
         for(auto s:acc.second){
             ans.push_back(s);
         }
         result.push_back(ans);
     }
     return result;
}
};

2.移出最多的同行或同列石头

image-20201128205813394

并查集,关键要找出什么和什么并起来

这一题很明显是点和点之间并(条件是两个点的行数或列数相同),并且在合并过程中求出每个簇下点的数量,

最后结果就是每个簇的点的数量-1,相加起来

class Solution {
public:
    int graph[10000]={0};
int size[10000]={0};
int  find(int i){
    if(graph[i]==i)return graph[i];
    else return graph[i]=find(graph[i]);
}
void Union(int i,int j){
    int p1=find(i);
    int p2=find(j);
    if(p1!=p2){
        if(size[p1]>size[p2]){
            graph[p2]=p1;
            size[p1]+=size[p2];
            size[p2]=0;
        }else{
            graph[p1]=p2;
            size[p2]+=size[p1];
            size[p1]=0;
        }
    }
}
int removeStones(vector<vector<int>>& stones) {
    int n=stones.size();
    for(int i=0;i<n;i++){
        graph[i]=i;
        size[i]=1;
    }
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            if(i!=j){
                if(stones[i][0]==stones[j][0]||stones[i][1]==stones[j][1]){
                    Union(i,j);
                }
            }
        }
    }
    int cnt=0;
    for(int i=0;i<n;i++){
        //cout<<size[i]<<" ";
        if(size[i]>0)cnt+=size[i]-1;
    }
    return cnt;

}

};

3.冗余连接

image-20201128205834999

这个问题意思是,现在有一个无向且有还的图,如何删掉一条边,使其成为无环图

---->也即,并查集合并的时候,如果发现有祖先使相等的,就说明该条边造成了环,则记录该边

class Solution {
public:
    int graph[10000]={0};
int size[10000]={0};
vector<int>result;
int  find(int i){
    if(graph[i]==i)return graph[i];
    else return graph[i]=find(graph[i]);
}
void Union(int i,int j){
    int p1=find(i);
    int p2=find(j);
    if(p1!=p2){
        graph[p2]=p1;
    }else{
        //说明发生了冲突,则把该边加入结果集
        result.push_back(i);
        result.push_back(j);
    }
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
     int n=edges.size();
     for(int i=0;i<=n;i++){
        graph[i]=i;
     }
     for(int i=0;i<n;i++){
         int l1=edges[i][0];
         int l2=edges[i][1];
         Union(l1,l2);
     }
     vector<int> v;
     v.push_back(result[result.size()-2]);
     v.push_back(result[result.size()-1]);
     return v;
}

};

4.被围绕的区域

image-20201128211624876

如何运用并查集?

1.将二维转化为一维,即i*row+j

2.创造虚拟节点row*col,将边界的和虚拟节点并,若非边界,就和上下左右并即可

int graph[10000]={0};
int  find(int i){
    if(graph[i]==i)return graph[i];
    else return graph[i]=find(graph[i]);
}
int cnt=0;
void Union(int i,int j){
    int p1=find(i);
    int p2=find(j);
    if(p1!=p2){
        graph[p2]=p1;
    }else{
        //说明改变无需存在就已经联通,让可重新操作的次数++;
        cnt++;
    }
}
bool  connected(int i,int j){
    return find(i)==find(j);
}
//可以将二维转化成一维
int turnNode(int i,int j,int cow){
    return i*cow+j;
}
//初始化函数
void init(int n){
    for(int i=0;i<n;i++)graph[i]=i;
}
int direction[4][2]={1,0,-1,0,0,1,0,-1};
void solve(vector<vector<char>>& board) {
   int row=board.size();
   if(row==0)return;
   int col=board[0].size();
   init(row*col+1);
   //创建一个虚拟节点,用于连接边界的O
   int virtualNode=row*col;
   for(int i=0;i<row;i++){
       for(int j=0;j<col;j++){
           if(board[i][j]=='O'){
               if(i==0||i==row-1||j==0||j==col-1){
                   //如果是边界的一个点,就和虚拟节点并
                   Union(virtualNode,turnNode(i,j,row));
               }else{
                   //否则就和上下左右并
                   for(auto k:direction){
                       int lx=i+k[0];
                       int ly=j+k[1];
                       if(lx>=0&&lx<row&&ly>=0&&ly<col&&board[lx][ly]=='O'){
                           Union(turnNode(i,j,row),turnNode(lx,ly,row));
                       }
                   }
               }
           }
       }
   }
   //填充,如果父类为虚拟node,则不变,否则为x
   for(int i=0;i<row;i++){
       for(int j=0;j<col;j++){
           cout<<graph[turnNode(i,j,row)]<<" ";
           if(!connected(virtualNode,turnNode(i,j,row))){
               board[i][j]='X';
           }
       }
       cout<<endl;
   }
}

5.统计参与通信的服务器

image-20201128211558849

image-20201128211606774

并查集,先算每一行,每一列,第一次出现的1的下标

接着,后面的电脑只需要并到前面记录的第一次出现的父类即可

用size代表该簇的大小

class Solution {
public:
int graph[1000000]={0};
int size[1000000]={0};
//查找
int  find(int i){
    if(graph[i]==i)return graph[i];
    else return graph[i]=find(graph[i]);
}
//合并
void Union(int i,int j){
    int p1=find(i);
    int p2=find(j);
    if(p1!=p2){
        graph[p2]=p1;
        size[p1]+=size[p2];
         size[p2]=0;
    }
}
bool  connected(int i,int j){
    return find(i)==find(j);
}
//可以将二维转化成一维
int node(int i,int j,int cow){
    return i*cow+j;
}
//初始化函数
void init(int n){
    for(int i=0;i<n;i++){
        graph[i]=i;
        size[i]=1;
    }
}
int countServers(vector<vector<int>>& grid) {
    int rows=grid.size();
    int cols=grid[0].size();
   // cout<<rows<<" "<<cols;
    init(rows*cols);
    map<int,int> row;
    map<int,int>col;
    for(int i=0;i<rows;i++){
        for(int j=0;j<cols;j++){
            if(grid[i][j]){
                //记录每个行,列第一次出现的位置
                if(row.count(i)==0)row[i]=j;
                if(col.count(j)==0)col[j]=i;
            }
        }
    }
    //合并
    int s=grid[0].size();
    for(int i=0;i<rows;i++){
        for(int j=0;j<cols;j++){
            if(grid[i][j]){
                if(row.count(i)){
                    Union(node(i,row[i],s),node(i,j,s));
                }
                if(col.count(j)){
                    Union(node(col[j],j,s),node(i,j,s));
                }
            }
        }
    }
    int cnt=0;
    for(int i=0;i<rows*cols;i++){
        cout<<size[i]<<" ";
        if(size[i]>1)cnt+=size[i];
    }
    return cnt;
}
};

6.保证图可以遍历

image-20201128211512765

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fnz7uDax-1606570817500)(D:\Typora\算法和数据结构\图算法\assets\1599381490231.png)]

图中有三种类型的边

对于Alice而言,她可以走类型一和1和3,

Bob可以走2和3

因此,我们可以先将类型为3的边合并,

再在3合并后的基础上,分别合并1和2,也即,分别构建属于Bob和Alice的并查集,

如果在合并过程中,发现两个点无法合并,就说明这条边可以删除

class Solution {
public:
    int getRoot(vector<int>& par,int x){
        int root = x;
        while(par[root]!=root){
            root = par[root];
        }
        while(par[x]!=root){
            int tmp = par[x];
            par[x] = root;
            x = tmp;
        }
        return root;
    }
    bool merge(vector<int>& par,int x,int y){
        int _x = getRoot(par,x);
        int _y = getRoot(par,y);
        if(_x!=_y){
            par[_x]=_y;
            return true;
        }
        return false;
    }
    int maxNumEdgesToRemove(int n, vector<vector<int>>& edges) {
        vector<int>par1 = vector<int>(n+1,0);
        vector<int>par2;
        int ans = 0;
        int cnt1 = n,cnt2;
        for(int i =1;i<=n;i++){
            par1[i] = i;
        }
        //先添加第三种类型的边
        for(int i = 0;i<edges.size();i++){
            if(edges[i][0]==3){
                if(!merge(par1,edges[i][1],edges[i][2]))
                    ans++;
                else
                    cnt1--;
            }
        }
        par2 = par1;
        cnt2 = cnt1;
        //再添加其余两种类型的边
        for(int i = 0;i<edges.size();i++){
            if(edges[i][0]==1){
                if(!merge(par1,edges[i][1],edges[i][2]))
                    ans++;
                else
                    cnt1--;
            }else if(edges[i][0]==2){
                if(!merge(par2,edges[i][1],edges[i][2]))
                    ans++;
                else
                    cnt2--;
            }
        }
        //如果连通集不为1,就说明不能完全遍历
        if(cnt1!=1||cnt2!=1)
            return -1;
        return ans;
    }
};

4.拓扑排序

1.课程表

59410727664

模板题:

class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
    //代表入度
    vector<int> in(numCourses,0);
    //邻接表
    vector<vector<int>> st(numCourses,vector<int>{});
    for (int i = 0; i <prerequisites.size() ; ++i) {
        in[prerequisites[i][0]]++;
        st[prerequisites[i][1]].push_back(prerequisites[i][0]);
    }
    queue<int> q;
    for (int j = 0; j <numCourses ; ++j) {
        if(!in[j])q.push(j);
    }
    while(!q.empty()){
        int top=q.front();
        q.pop();
        for (int i = 0; i <st[top].size() ; ++i) {
            in[st[top][i]]--;
            if(!in[st[top][i]]){
                q.push(st[top][i]);
            }
        }
    }
    for (int k = 0; k <numCourses ; ++k) {
        if(in[k])return false;
    }
    return true;
}
};

2.矩阵中的最长递增路径

image-20201128204842197

这是一道动态规划的题目,但用拓扑排序更方便

要求最长递增路径,比如从最有下角的两个1,

1->8

1->{6,2}

其实这里的1相当于第一层

接着2,6,8就是第二层

所以,最长的路径其实就是算层数,

即,每次拓扑排序遍历的是一整层的数据,而不是一个个遍历

class Solution {
    public int longestIncreasingPath(int[][] matrix) {
        if (matrix==null||matrix.length==0){
            return 0;
        }        
        int[][] count = new int[matrix.length][matrix[0].length];

        //统计每个点的入度用count数组保存,因为是递增,所以如果在上下左右,每发现一个比当前点小的数,当前点入度+1
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                for (int[] d : direction) {
                    if (longestIncreasingPathVerify(matrix, i + d[0], j + d[1]) && matrix[i + d[0]][j + d[1]] < matrix[i][j]) {
                        count[i][j]++;
                    }
                }
            }
        }
        Deque<int[]> deque = new LinkedList<>();
        //count数组中所有入度为0的点加入队列
        for (int i = 0; i < count.length; i++) {
            for (int j = 0; j < count[i].length; j++) {
                if (count[i][j] == 0) {
                    deque.add(new int[]{i, j});
                }
            }
        }
        int ans = 0;
        while (!deque.isEmpty()) {
            ans++;
            //这个跟课程表I那个题不一样,需要一层一层的出列,而不是一个一个的出,因为课程表那个不关心队列长度
            for (int size = deque.size(); size > 0; size--) {
                int[] poll = deque.poll();
                for (int[] d : direction) {
                    if (longestIncreasingPathVerify(matrix, poll[0] + d[0], poll[1] + d[1]) && matrix[poll[0] + d[0]][poll[1] + d[1]] > matrix[poll[0]][poll[1]]) {
                        if (--count[poll[0] + d[0]][poll[1] + d[1]] == 0) {
                            deque.add(new int[]{poll[0] + d[0], poll[1] + d[1]});
                        }
                    }
                }
            }
        }
        return ans;
    }

    private boolean longestIncreasingPathVerify(int[][] matrix, int i, int j) {
        return i >= 0 && j >= 0 && i < matrix.length && j < matrix[i].length;
    }
}

5.最小生成树

1.公路村村通

image-20201128164650329

输入样例:

6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3

输出:

10

题目分析

就是根据所有的边,构建一颗最小生成树,使所需费用最低,这里用的是Kruskal 算法:

#include <bits/stdc++.h>
using namespace std;
//结构Edge 代表边
struct Edge{
    int a,b,w;
    Edge(int a,int b ,int w):a(a),b(b),w(w){}
    bool operator < (Edge o)const{
        return w<o.w;
    }
};
vector<Edge> g;
vector<int> fa;
//并查集路径压缩查找
int find(int x){
    return x==fa[x]?x:fa[x]=find(fa[x]);
}
//kruska算法
int kruska(int n){
    sort(g.begin(),g.end());
    int ans=0;
    int cnt=n;
    for(Edge e:g){
        int fax= find(e.a);
        int fay= find(e.b);
        if(fax!=fay){
            cnt--;
            fa[fax]=fay;
            ans+=e.w;
        }
    }
    return cnt==1?ans:-1;
}
int main(){
    int n,m;
    cin>>n>>m;
    int a,b,value;
    fa=vector<int>(n+1,0);
    for (int i = 1; i <=n ; ++i) {
        fa[i]=i;
    }
    for(int i=0;i<m;i++){
        cin>>a>>b>>value;
        g.push_back(Edge(a,b,value));
    }
    cout<<kruskal(n);
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值