图论初步

Part 2

  • 图的表示
  • 广度优先搜索

图搜索

是(有向或无向)图,按一定的顺序把 的每个点都到过visit或者说访问过,每条边都走过traverse,称为图搜索或图遍历。

重要的图搜索算法有两个:广度优先搜索breadth-first search, BFS深度优先搜索depth-first search, DFS

BFS

为连通图, 。以 为起点对图 进行的 BFS,像水波那样一层一层向外扩展,由近及远,把 的每个顶点和每条边都“搜到”;同时把从 到每个点的距离算出来。

  • 白点:尚未被发现discovered
  • 灰点:已被发现但从它发出的边还没走过。在队列里。
  • 黑点:已被发现且从它发出的边都走过了。已出队。
  • 蓝边:导向新发现的顶点。它们构成 的生成树(称为 BFS 树)。

队列里的点到起点的距离相差不超过

BFS 的实现

写法一

const int maxn = 1e5 + 5;
vector<int> g[maxn];
int d[maxn];
void init() {
    memset(d, -1, sizeof d);
}
void bfs(int s) {
    queue<int> q;
    q.push(s);
    d[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : g[u])//扩展
            if (d[v] == -1) {
                d[v] = d[u] + 1;
                q.push(v);
            }
    }
}

写法二

const int maxn = 1e5 + 5;
vector<int> g[maxn];
int d[maxn];
bool vis[maxn];
struct Path { int to, len; };

void bfs(int s) {
    queue<Path> q;
    q.push({s, 0});
    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        if (vis[t.to]) continue;
        vis[t.to] = true;
        d[t.to] = t.len;
        for (int v : g[t.to])//扩展
            q.push({v, t.len + 1});
    }
}

习题 找倍数

给你正整数 ),求一个不超过 位的正整数 满足 的倍数并且 写成十进制数只含有数字

保证有解。

解法

把只含有数字 的正整数按除以 的余数分成 类。

为例,若 ,那么在 后面加上数字 得到的数除以 的余数是 ,在 后面加上数字 的到的数除以 .

考虑以下有向图

代码

struct X {
    int r;
    string s;
};
void bfs(int n) {
    vector<bool> vis(n);
    queue<X> q;
    q.push({1 % n, "1"});
    while (!q.empty()) {
        X t = q.front(); q.pop();
        if (vis[t.r]) continue;
        vis[t.r] = true;
        if (t.r == 0) {
            cout << t.s << '\n';
            break;
        }
        for (int i : {0, 1})
            q.push({(t.r * 10 + i) % n, t.s + char('0' + i)});
    }
}

习题 abc363_e 沉没的土地

有一个尺寸是 的矩形岛屿,四周都是海水。
岛被划成 列的网格,第 行第 列的格子现在海拔高度是

从现在开始海平面每年上升

跟海水或者已被淹没的格子相邻并且不高于海平面的格子会被淹没。

给定正整数 ,对于每个 ,求 年之后尚未被淹没的格子的数量。

限制

例子

的网格,

10 2 10
3  1 4
10 5 10

一年后,没有格子被淹没,有 个格子尚未被淹没。
两年后,有 个格子尚未被淹没。

10 x 10
3  x 4
10 5 10

三年后,有 个格子尚未被淹没。

10 x 10
x  x 4
10 5 10

四年以后,有 个格子尚未被淹没。

10 x 10
x  x x
10 5 10

五年以后,有 个格子尚未被淹没。

10 x 10
x  x x
10 x 10

思路

  • 对于 ,计算有多少个格子到 年后被淹没。

  • 找出那些跟 年后已被淹没的格子相邻的海拔高度是 的格子。

  • 这些格子首当其冲,随后向内传染,使跟它们连通并且海拔高度不超过 的格子被淹没。

代码

const int maxa = 1e5+5;
struct GZ { int r, c; }; //格子
vector<GZ> g[maxa];
int a[1002][1002];
bool sink[1002][1002];
int main() {
    int H, W, Y; cin >> H >> W >> Y;
    for (int i = 1; i <= H; i++)
        for (int j = 1; j <= W; j++) {
            cin >> a[i][j];
            g[a[i][j]].push_back({i, j});
        }
    for (int i = 0; i <= H + 1; i++)
        for (int j = 0; j <= W + 1; j++)
            if (i == 0 || j == 0 || i == H + 1 || j == W + 1)
                sink[i][j] = true;
    
    int dir[4][2] = {1, 0, -1, 0, 0, 1, 0, -1};
    int ans = H * W;
    for (int i = 1; i <= Y; i++) {
        queue<GZ> q;
        for (GZ p: g[i])
            for (int j = 0; j < 4; j++)
                if (sink[p.r + dir[j][0]][p.c + dir[j][1]]) { q.push(p); break; }
        // BFS
        while (!q.empty()) {
            GZ p = q.front(); q.pop();
            if (sink[p.r][p.c]) continue;
            sink[p.r][p.c] = true; ans--;
            for (int j = 0; j < 4; j++)
                if (a[p.r + dir[j][0]][p.c + dir[j][1]] <= i)
                    q.push({p.r + dir[j][0], p.c + dir[j][1]});
        }
        cout << ans << '\n';
    }
}

习题 假期计划

给定有 个点 条边的图 ,点从 编号。对 ,点 有分数 。今要从点 中选择四个相异的点 ,使得序列 中每相邻两点在图 上距离都不大于

四点的分数之和的最大值。

保证存在满足条件的四个点。

限制

分析

  • 观察右图,注意到行程的对称性。考虑枚举中间两点
  • 对每个点 ,在距离点 和点 都不超过 的点中取分数最大的三个,成集合
  • 可从 中选而 可从 中选。

代码

const int maxn = 2500 + 5;
int n, m, k;
vector<int> g[maxn];
long long p[maxn];
vector<int> s[maxn];
int d[maxn][maxn];
const int INF = 1e9;

void bfs(int s) {
  for (int i = 1; i <= n; i++)
    dist[s][i] = INF;
  queue<int> q;
  q.push(s);
  d[s][s] = 0;
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (int v : g[u])
    if (d[s][v] == INF) {
      d[s][v] = d[s][u] + 1;
      q.push(v);
    }
  }
}

bool cmp(int i, int j) {
  return p[i] > p[j];
}
int main() {
  cin >> n >> m >> k;
  for (int i = 2; i <= n; i++) cin >> p[i];
  for (int i = 0; i < m; i++) {
    int u, v; cin >> u >> v;
    g[u].push_back(v); g[v].push_back(u);
  }
  for (int i = 1; i <= n; i++) bfs(i);

  vector<int> a;
  for (int i = 2; i <= n; i++)
    if (d[1][i] <= k + 1) a.push_back(i);
  sort(a.begin(), a.end(), cmp);
  
  for (int i = 2; i <= n; i++)
    for (int v : a)
      if (v != i && d[i][v] <= k + 1) {
        cand[i].push_back(v);
        if (cand[i].size() == 3) break;
      }

  long long ans = 0;

  for (int c = 2; c <= n; c++)
    for (int d = c + 1; d <= n; d++)
      if (d[c][d] <= k + 1)
        for (int b : cand[c])
          for (int e : cand[d])
            if (b != d && e != c && b != e)
              ans = max(ans, p[b] + p[c] + p[d] + p[e]);
  cout << ans << '\n';         
}

[CSP-J 2023] 旅游巴士

某景区有 个地点,地点 为入口,地点 为出口。从一天当中景区开门的时间(记为 0 时刻)起,每隔 单位时间便有一辆巴士到达入口,同时有一辆巴士从出口驶离。

单向道路连接这些地点。走过一条道路需要一单位时间。每条道路有一个开放时间 :游客只有不早于 时刻才能通过道路

小 Z 希望乘坐巴士到达入口,走到出口,再乘坐巴士离开,因此他到达和离开景区时间都必须是 的非负整数倍。小 Z 在离开景区之前会一直移动而不在任何地点或道路上逗留。

求小 Z 可能离开景区的最早时间。

限制

一个特殊情形

如果在所有道路都开放之后进入景区,那么问题化为在一个有 个点和 条边的有向图上求最短路。

样例:

解法

时刻开始,沿着时间轴一秒一秒推进(设一单位时间是一秒)。
对于每个时刻 ,令 为可能在 时刻到达的点的列表。

在上述有 个点和 条边的有向图上进行(特殊的)BFS,在 BFS 扩展的过程中填充诸列表

注意到

代码

struct E {int to, a;};
const int maxn = 1e4 + 5;
const int maxa = 1e6 + 5;
vector<E> g[maxn];
vector<int> V[2 * maxa + 100];
bool vis[maxn][100];
int n, m, k;

int bfs() {
  V[0].push_back(1);
  for (int t = 0; t < 2e6 + 100; t++)
    // 从t时刻能到的点向(时间上地)后扩展
    for (int u : V[t]) {
      if (vis[u][t % k]) continue;
      vis[u][t % k] = 1;
      if (u == n && t % k == 0)
        return t;
      for (E e : g[u])
        if (e.a <= t) V[t + 1].push_back(e.to);
        else {
          int nt = t + (e.a - t + k - 1) / k * k + 1;
          V[nt].push_back(e.to);
        }
    }
  return -1;
}

01-BFS

BFS 常用来计算从一个状态到另一个状态最少需要走几。在一些问题中有的走法算一步,有的走法不算步数。用图论的语言来描述:设 是一个带权有向图。边的权值是 。给定起点 和终点 ,求从 的最短路的长度。

上述问题也可以用 BFS 来解决:当顶点 出队后,要从它向外扩展一条边

  • 的权值是 ,则把点 从末尾加入队列;
  • 的权值是 ,则把点 从开头加入队列。

为此我们要把普通 DFS 中使用的队列换为双端队列double-ended queue:可以在开头或结尾添加或弹出元素的队列。

C++ 标准库里有现成的双端队列 std::deque。

std::deque

支持下列队列操作

  • 从末尾添加元素 .push_back()
  • 从开头弹出元素 .pop_front()
  • 从末尾弹出元素 .pop_back()
  • 从开头添加元素 .push_front()

实际上,deque 和 vector 相似,是一种通用的序列容器,也支持用下标访问元素。不过 deque 的结构比 vector 复杂,常数也较大;所以通常我们只把它用作双端队列。

例题 abc213_e Stronger Takahashi

有一个 列的网格。每个格子或者可通过,或者是障碍物。

高桥要从左上角的格子走到右下角的格子。每一步他可走到上下左右相邻且可通过的格子里去。他不能走出网格,也不能走到障碍物格子里去。不过,高桥力气很大,他打一拳能把一个 方形区域里的障碍物全部清除,使这些格子可通过。

求高桥从左上角的格子走到右下角的格子至少要打几拳。

限制
  • 左上角和右下角的格子都可通过。

输入

5 5
..#..
#.#.#
##.##
#.#.#
..#..

输出:1

..#..
#.**#
##**#
#.#.#
..#..

分析

假设高桥已经确定了路线。他打拳的目标是消除路线上的障碍物。沿着既定的路线走,遇到障碍物时才考虑如何打一拳把它消除。

代码

struct point {
  int r, c, d;
};
int dir[4][2] = {0,1,0,-1,1,0,-1,0};
bool vis[500][500];
//网格行列编号从0开始
char s[500][505];
int h, w;

int main() {
  int h, w;
  cin >> h >> w;
  for (int i = 0; i < h; i++)
    cin >> s[i];
  01_bfs();
  return 0;
}
void 01_bfs() {
  deque<point> q;
  q.push_back({0, 0, 0});
  while (!q.empty()) {
    auto cur = q.front(); q.pop_front();
    if (vis[cur.r][cur.c]) continue;
    vis[cur.r][cur.c] = true;
    if (cur.r == h - 1 && cur.c == w - 1) {
      cout << cur.d; return;
    }
    vis[cur.r][cur.c] = true;
    for (int i = 0; i < 4; i++) {
      int r = cur.r + dir[i][0], c = cur.c + dir[i][1];
      if (0 <= r && r < h && 0 <= c && c < w) {
        if (s[r][c] == '.') {
          q.push_front({r, c, cur.d});
        } else {// s[r][c] == '#'
          for (int dr = -1; dr <= 1; dr++)
            for (int dc = -1; dc <= 1; dc++) {
              int nr = r + dr, nc = c + dc;
              if (0 <= nr && nr < h && 0 <= nc && nc < w)
                q.push_back({nr, nc, cur.d + 1});
            }
        }
      }
    }
  }
}