《算法导论(原书第3版)》第22章部分题目解答

第22章 基本的图算法

22.1 图的表示

22.1-4

用一个二维矩阵v来记录一个边是否出现过,若出现过,不再加入邻接链表中。
用vector e, newe来模拟旧邻接链表与新邻接链表
部分c++代码:

//用于处理有向图,若处理无向图将判断条件修改为v[i][j]||v[j][i],且每扫描一个边后v[i][j]=v[j][i]=1;
for(int i=1;i<n;i++){ 
    for(auto j:e[i]){
        if(i == j||v[i][j]) continue;
        newe[i].push_back(j);
        v[i][j] = 1;
    }
 }

22.1-5

使用邻接矩阵M保存\(G^2\)的信息,用临界链表保存\(G\)的信息。
用vector e模拟临界链表。
时间复杂度为\(O(E^2)\),不包含重边时时间复杂度为\(O(V^3)\)
部分c++代码:

//用于处理有向图,处理无向图时,相应部分改为M[i][j]=M[j][i]=1,M[i][k]=M[k][i]=1
for(int i=1;i<=n;i++){
    for(auto j:e[i]){
        M[i][j] = 1;
        for(auto k:e[j]){
            M[i][k] = 1;
        }
    }
}

22.1-6

思路:
根据通用汇点的定义可知,一张图中最多只能有一个通用汇点。所以我们可以先用O(n)的时间,将可能为通用汇点的那个点找出来,再用O(n)的时间检查该点是否满足条件。

int i = 1, j = 1;
while(i<=n && j<=n){
  if(M[i][j])i++;
  else j++;
}
if(i==n+1)cout<<"无通用汇点";
else {
  int ok = M[i][i]? 0:1;
  for(int j = 1; j<=n; j++){
    if(i==j)continue;
    if(!M[i][j]){ok = 0;break;}
  }
  if(ok) cout<<i;
  else cout<<"无通用汇点";
}

22.1-7

\(BB^T\)生成的新矩阵为C,其元素为\(c_{ij}\)

当i == j时

\(c_{ij} = \sum_{k=1}^{|E|}b_{ik}b_{ki}^T = \sum_{k=1}^{|E|}(b_{ik})^2\)
根据\(b_{ij}定义,当i与边k相连\)(b_{ik})^2 = 1\(,否则\)(b_{ik})^2 = 0\( 所以\)c_{ij}$表示i,j连边数

当i!=j时

\(c_{ij} = \sum_{k=1}^{|E|}b_{ik}b_{kj}^T = \sum_{k=1}^{|E|}b_{ik}b_{jk}\)
当k连接i与j时,\(b_{ik},b_{jk}\)中必有一个值为1,另一个值为-1,此时\(b_{ik}b{jk} = -1\)
否则\(b_{ik}b_{jk} = 0\)
所以\(c_{ij}\)表示点i与点j之间连边的相反数

22.2 广度优先搜索

22.2-6

举例:

原图:
点V:A,B,C,D,E,F
边E:A->B,A->C,B->E,B-F,C->E,C->F
对于该图,取源结点s为A,取一组树边\(E_\pi\)为:A->B,A->C,B->E,C->F

22.2-7

问题即判断一个图是否为二分图,用染色法BFS一遍即可。编号假设从0开始
c++代码:

const int maxn = ...;
vector<int> e[maxn];
int c[maxn];//记录点的颜色,1表示“娃娃脸”,2表示“高跟鞋”
bool bfs(int s){
  queue<int> q; q.push(s); c[s] = 1;
  while(!q.empty()){
    int x = q.front(); q.pop();
    for(auto y: e[x]){
      if(c[y] == 0){
        q.push(y);
        c[y] = 3-c[x];
      } else if(c[y] == c[x]){
        return false;
      }
    }
  }
  return true;
}
int main(){
  cin>>n>>m;
  while(m--){
    int u,v; cin>>u>>v;
    e[u].push_back(v);
    e[v].push_back(u);
  }
  int f = 1;
  //遍历各个连通分支
  for(int i = 0; i<n; i++){
    if(c[i]==0){
      if(!bfs(i)){
        f = 0; break;
      }
    }
  }
  if(f) cout<"YES";
  else cou<<"NO";
}

22.2-8

正确性:

做两遍BFS,具体做法如下

  1. 从任意一个结点出发,做一遍BFS,并记录距出发点最远的结点p。
  2. 从p出发,再做一遍BFS,并记录距p最远结点q,从p到q路径即为树的一条直径。

证明:

因为p一定是直径一端(否则总能找到一条更长链,与直径定义矛盾),那么距离p最远的q自然是直径另一端

时间复杂度:

BFS的复杂度为O(V+E),又因为是一颗心树,所以复杂度为O(V)。

部分c++代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
vector<int> e[maxn];//存边
int d[maxn];//存距离
void bfs(int s,int &t){
  memset(d,-1,sizeof(d));
  queue<int> q; q.push(s); d[s] = 0;
  while(q.size()){
    t = q.front(); q.pop();
    for(auto y:e[x]){
      if(d[y] == -1) d[y] = d[t]+1, q.push(y);
    }
  }
}
int solve(){
  int p.q;//直径两端
  bfs(0,p); bfs(p,q);
  return d[q];
}

该问题还有树形DP的解法,在此不再赘述。

22.4 拓扑排序

22.4-2

思路

做一遍dfs,为了保证不会重复搜素要加入记忆化(实际上是一个DP)。

时间复杂度

每个点、每条边只会在搜索过程中被遍历一次,所以复杂度为O(V+E),满足线性复杂度的要求。
由于题目中给的点是用char类型字符来标识的,所以需要用map<char,int>做一个映射。

部分c++代码

#include <bits/stdc++.h>
using namespace std;
const int max = ...;
map<char,int> mp;
vector<int> e[maxn];
int dp[maxn];
int Begin, End;
int dfs(int t){
  if(dp[t]!=-1) return dp[t];
  dp[t] = 0;
  for(auto j: e[t]){
    dp[t] += dfs(i);
  }
  return dp[t];
}
void solve(){
  memset(dp,-1,sizeof(dp));
  for(int i = 1; i<=n; i++){
    if(a[i] == mp['s']) Begin = i;
    if(a[i] == mp['t']) End = i, dp[i] = 1;
  }
  return dfs(Begin);
}

22.4-4

不正确。

举例:

对于图\(G(V,E)\)
点V:a,b,c,d
点E:a->d,a->b,b->c,d->c,c->a
从c开始做一遍拓扑排序,它的拓扑序应该是cabd,有(b,c),(d,c)2个坏边。
但是如果拓扑序是abcd的话,只有(c,a)1个坏边。

22.4-5

本质上是一个bfs算法

部分c++代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = ...;
int deg[maxn], cnt = 0;
vector<int> e[maxn];
void topsort(){
  queue<int> q;
  for(int i = 1; i<=n; i++) if(deg[i]==0) q.push(i);
  while(!q.empty()){
    int x = q.front(); q.pop();
    a[++cnt] = x;
    for(auto y:e[x]){
      if(--deg[y]==0)q.push(y);
    }
  }
}

如果有环,环上的点必然不会加入到队列中,这样我们无法获得一个完整的拓扑序。

22.5 强连通分量

22.5-3

不正确。

举例:

对于图\(G(V,E)\)
V:a,b,c
E:b->a,b->c,c->b
如果我们在第一遍扫描的时候从b开始,而b先进入c,这样我们在第二遍扫描的时候,会从b开始,并会把a,b判定在同一个强连通分量中,这显然是错误的。

22.5-7

思路:

先跑一遍tarjan算法,求出图中的强连通分量并缩点。对于这些SCC缩点做拓扑排序,直到拓扑排序完成,若任意时刻队列中元素大于1,则说明不是单连通图,否则,该图为半连通图。

证明:

对于强连通分量,它们显然是符合半连通要求的。对于缩点后的图,在进行拓扑排序时,若队列中同时出现了2个(或以上)点,这些点之间显然是无法相互到达的,不满足半连通要求。否则,对于每个后来的点,它们可以由每个之前的点到达;相应地,之前的每一个点都可以到达后来的点。所以当我们完成拓扑排序时,每个点都可以被证明与其他的点至少有一个路径。所以整张图是半连通的。

时间复杂度:

由于tarjan算法与拓扑排序均为线性时间,所以该算法为线性时间。

部分c++代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn = ...;
vector<int> e[maxn];//存原来的边
vector<int> ne[maxn];//存缩点后的边
int dfs[maxn],low[maxn],Stack[maxn],ins[maxn],c[maxn];
vector<int> scc[maxn];//存编号为i的强连通分量的所有结点。
int n,top,cnt,num;//cnt记录有多少个强连通分量
void tarjan(int x){
  dfn[x] = low[x] = ++num;
  Stack[++top] = x, ins[x] = 1;
  for(auto y: e[x]){
    if(!dfn[y]){
      tarjan(y);
      low[x] = min(low[x],low[y]);
    } else if(ins[y]){
      low[x] = min(low[x],dfn[y]);
    }
  }
  if(dfn[x] == low[x]){
    cnt++; int y;
    do{
      y = Stack[top--]; 
      ins[y] = 0;
      c[y] = cnt;
      scc[cnt].push_back(y);
    }while(x!=y);
  }
}
int deg[maxn];
void topsort(){
  queue<int> q;
  for(int i = 1; i<=n; i++) if(deg[i]==0) q.push(i);
  while(!q.empty()){
    if(q.size() > 1) return false;
    int x = q.front(); q.pop();
    a[++cnt] = x;
    for(auto y:e[x]){
      if(--deg[y]==0)q.push(y);
    }
  }
  return true;
}
void solve(){
  for(int i = 1; i<=n; i++){
    if(!dfn[i]) tarjan(i);
  }
  for(int i = 1; i<=cnt; i++){
    for(auto x: scc[i]){
      for(auto y: e[x]){
        if(c[y] == c[x]) continue;
        ne[i].push_back(c[y]);
      }
    }
  }
  if(topsort())cout<<"是半连通图";
  else cout<<"不是半连通图";
}

思考题

22-4

思路:

先通过tarjan算法缩点。然后,对于每个SCC,将它当前的标记值记为该SCC所有点中标记最小的点的标记值,然后跑一遍dfs就可以了。

部分c++代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn = ...;
vector<int> e[maxn];
vector<int> ne[maxn];
int dfs[maxn],low[maxn],Stack[maxn],ins[maxn],c[maxn];
vector<int> scc[maxn];//存编号为i的强连通分量的所有结点。
int ans[maxn], scc_ans[maxn];
int n,top,cnt,num;//cnt记录有多少个强连通分量
void tarjan(int x){
  dfn[x] = low[x] = ++num;
  Stack[++top] = x, ins[x] = 1;
  for(auto y: e[x]){
    if(!dfn[y]){
      tarjan(y);
      low[x] = min(low[x],low[y]);
    } else if(ins[y]){
      low[x] = min(low[x],dfn[y]);
    }
  }
  if(dfn[x] == low[x]){
    cnt++; int y, t = x;
    do{
      y = Stack[top--]; 
      ins[y] = 0;
      c[y] = cnt;
      scc[cnt].push_bakc(y);
      scc_ans[cnt]=min(t,y);
    }while(x!=y);
  }
}
int v[maxn];
void dfs(int x){
  v[x] = 1;
  for(auto y: ne[x]){
    if(!v[y]) dfs(y);
    scc_ans[x] = min(scc_ans[x],scc_ans[y]);
  }
}
void solve(){
  for(int i = 1; i<=n; i++)if(!dfn[i])tarjan(i);
  for(int i = 1; i<=cnt; i++){
    for(auto x:scc[i]){
      for(auto y:ne[x]){
        if(c[y]==c[x]) continue;
        ne[i].push_back(c[y]);
      }
    }
  }
  for(int i = 1;i<=cnt;i++)if(!v[i])dfs(i);
  for(int i = 1;i<=n;i++)ans[i]=scc_ans[c[i]];
}
posted @ 2019-12-31 16:53  baoliang  阅读(716)  评论(0)    收藏  举报