AcWing搜索与图论

1. DFS

数据结构:stack
空间复杂度:O(h)
不具最短性
每个DFS都对应一个搜索树

  • 排列数字

给定一个整数 n,将数字 1∼n排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

int n;
int p[N];
bool st[N];

void dfs(int u) {
	if (u == n) {
		for (int i = 0; i < n; i ++ ) {
			cout << p[i] << " ";
		}
		cout << endl;
		return ;
	}
	for (int i = 1; i <= n; i ++ ) {
		if (!st[i]) {
			p[u] = i;
			st[i] = 1;
			dfs(u + 1);
			st[i] = 0;
		}
	}
}

void solve() {
	cin >> n;
	dfs(0);
}

int main() {
	int t = 1;
//	cin >> t;
	while (t -- ) {
		solve();
	}

	return 0;
}
  • n-皇后问题
 int n;
  char g[N][N];
  bool row[N], col[N], dg[N], udg[N];
  
  void dfs(int x, int y, int s) { // x,y是横纵坐标,s是放到棋盘的元素个数
      if (y == n) { // 搜到每行最后一个,换行继续搜
          y = 0; 
          x ++ ;
      }
      if (x == n) { // 搜到最后一行,停止
          if (s == n) { // 元素个数等于n,找到一组符合题意的解
              for (int i = 0; i < n; i ++ ) {
                  puts(g[i]);
              }
              puts("");
          }
          return ;
      }
      
      // 不放元素,直接递归到下一个格子
      dfs(x, y + 1, s);
      
      // 放元素
      if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) { // 反对角线+n是为了防止数组下标越界
          g[x][y] = 'Q'; // 赋值
          row[x] = col[y] = dg[x + y] = udg[x - y + n] = 1; // 标记
          dfs(x, y + 1, s + 1); // 递归到下一个格子
          row[x] = col[y] = dg[x + y] = udg[x - y + n] = 0; // 恢复现场
          g[x][y] = '.';
      }
  }
  
  int main() {
      cin >> n;
      for (int i = 0; i < n; i ++ ) {
          for (int j = 0; j < n; j ++ ) {
              g[i][j] = '.';
          }
      } 
      dfs(0, 0, 0);
      
      return 0;
  }
DFS之连通性模型
  • 迷宫

一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n∗n的格点组成,每个格点只有2种状态,.#,前者表示可以通行后者表示不能通行。

同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。

如果起点或者终点有一个不能通行(为#),则看成无法办到。

注意:A、B不一定是两个不同的点。

输入格式

第1行是测试数据的组数 k,后面跟着 k组输入。

每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n∗n 的。

接下来是一个 n∗n的矩阵,矩阵中的元素为.或者#

再接下来一行是 4 个整数 ha,la,hb,lb,描述 A处在第 ha行, 第 la列,B处在第 hb行, 第 lb 列。

注意到 ha,la,hb,lb 全部是从 0 开始计数的。

输出格式

k行,每行输出对应一个输入。

能办到则输出“YES”,否则输出“NO”。

const int N = 110;
char g[N][N];
bool st[N][N];
int n;
int xa, ya, xb, yb;

bool dfs(int x, int y) {
    if (g[x][y] == '#') {
        return false;
    }
    
    if (x == xb && y == yb) {
        return true;
    }
    
    st[x][y] = true;
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    for (int i = 0; i < 4; i ++ ) {
        int nx = x + dx[i], ny = y + dy[i];
        if (nx < 0 || nx >= n || ny < 0 || ny >= n) {
            continue;
        }
        if (st[nx][ny]) {
            continue;
        }
        if (dfs(nx, ny)) {
            return true;
        }
    }
    
    return false;
}

void solve() {
    memset(st, 0, sizeof st);
    cin >> n;
    for (int i = 0; i < n; i ++ ) {
        cin >> g[i];
    }
    cin >> xa >> ya >> xb >> yb;
    if (dfs(xa, ya)) {
        puts("YES");
    } else {
        puts("NO");
    }
}

int main() {
    int t;
    cin >> t;
    while (t -- ) {
        solve();
    }
    
    return 0;
}
  • 红与黑

有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。

你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。

请写一个程序,计算你总共能够到达多少块黑色的瓷砖。

输入格式

输入包括多个数据集合。

每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y方向瓷砖的数量。

在接下来的 H行中,每行包括 W个字符。每个字符表示一块瓷砖的颜色,规则如下

1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。

当在一行中读入的是两个零时,表示输入结束。

输出格式

对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。

char g[N][N];
bool st[N][N];
int n, m;

int bfs(int x, int y) {
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    queue<PII> q;
    q.push({x, y});
    st[x][y] = 1;
    int cnt = 1;
    while (q.size()) {
        auto t = q.front();
        q.pop();
        for (int i = 0; i < 4; i ++ ) {
            int nx = t.first + dx[i], ny = t.second + dy[i];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
                continue;
            }
            if (st[nx][ny]) {
                continue;
            }
            if (g[nx][ny] != '.') {
                continue;
            }
            q.push({nx, ny});
            st[nx][ny] = 1;
            cnt ++ ;
        }
    }
    
    return cnt;
}

int main() {
    while (cin >> m >> n, n || m) {
        memset(st, 0, sizeof st);
        for (int i = 0; i < n; i ++ ) {
            cin >> g[i];
        }
        int x, y;
        for (int i = 0; i < n; i ++ ) {
            for (int j = 0; j < m; j ++ ) {
                if (g[i][j] == '@') {
                    x = i, y = j;
                }
            }
        }
        cout << bfs(x, y) << endl;
    }
    
    return 0;
}
DFS之搜索顺序
  • 马走日

马在中国象棋以日字形规则移动。

请编写一段程序,给定 n∗m大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。

输入格式

第一行为整数 T,表示测试数据组数。

每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。

输出格式

每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。

bool st[N][N];
int res;
int n, m;

void dfs(int x, int y, int cnt) {
    int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
    int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
    if (cnt == n * m) { // 退出条件
        res ++ ;
        return ;
    }
    st[x][y] = 1; // 标记
    for (int i = 0; i < 8; i ++ ) {
        int nx = x + dx[i], ny = y + dy[i];
        if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
            continue;
        }
        if (st[nx][ny]) {
            continue;
        }
        dfs(nx, ny, cnt + 1);
    }
    st[x][y] = 0; // 恢复现场
}

void solve() {
    int x, y;
    cin >> n >> m >> x >> y;
    memset(st, 0, sizeof st);
    res = 0;
    dfs(x, y, 1);
    cout << res << endl;
    
}

int main() {
    int t;
    cin >> t;
    while (t -- ) {
        solve();
    }
    
    return 0;
}
  • 单词接龙

单词接龙是一个与我们经常玩的成语接龙相类似的游戏。

现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”,每个单词最多被使用两次。

在两个单词相连时,其重合部分合为一部分,例如 beast 和 astonish ,如果接成一条龙则变为 beastonish。

我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度,例如 at 和 atide 间不能相连。

输入格式

输入的第一行为一个单独的整数 n 表示单词数,以下 n行每行有一个单词(只含有大写或小写字母,长度不超过20),输入的最后一行为一个单个字符,表示“龙”开头的字母。

你可以假定以此字母开头的“龙”一定存在。

输出格式

只需输出以此字母开头的最长的“龙”的长度。

int n;
int g[N][N]; // 存单词之间的重叠部分的最小长度
int used[N]; // 存单词用过几次
string w[N];
int res;

void dfs(string dragon, int last) {
    res = max((int)dragon.size(), res); // 最大值,dragon.size()为当前重复合并长度
    used[last] ++ ;
    for (int i = 0; i < n; i ++ ) {
        if (g[last][i] && used[i] < 2) { // 可以连并且使用次数小于2
            dfs(dragon + w[i].substr(g[last][i]), i);
        }
    }
    used[last] -- ; // 恢复现场
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i ++ ) {
        cin >> w[i];
    }
    char start;
    cin >> start;
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < n; j ++ ) {
            string a = w[i], b = w[j];
            for (int k = 1; k <= min(a.size(), b.size()); k ++ ) {
                if (a.substr(a.size() - k, k) == b.substr(0, k)) {
                    g[i][j] = k;
                    break;
                }
            }
        }
    }
    
    for (int i = 0; i < n; i ++ ) {
        if (w[i][0] == start) {
            dfs(w[i], i);
        }
    }
    cout << res << endl;
    
    return 0;
}
  • 分成互质组

给定 n 个正整数,将它们分组,使得每组中任意两个数互质。

至少要分成多少个组?

输入格式

第一行是一个正整数 n。

第二行是 n个不大于10000的正整数。

输出格式

一个正整数,即最少需要的组数。

int n;
int p[N];
int group[N][N];
bool st[N];
int ans = N;

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

bool check(int group[], int gc, int i) {
    for (int j = 0; j < gc; j ++ ) {
        if (gcd(p[group[j]], p[i]) > 1) {
            return false;
        }
    }
        
    return true;
}

void dfs(int g, int gc, int tc, int start) {
    if (g >= ans) {
        return;
    }
    if (tc == n) {
        ans = g;
    }
    bool flag = true;
    for (int i = start; i < n; i ++ ) {
        if (!st[i] && check(group[g], gc, i)) {
            st[i] = true;
            group[g][gc] = i;
            dfs(g, gc + 1, tc + 1, i + 1);
            st[i] = false;

            flag = false;
        }
    }
        

    if (flag) {
        dfs(g + 1, 0, tc, 0);
    }
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i ++ ) {
        cin >> p[i];
    }
    dfs(1, 0, 0, 0);
    cout << ans << endl;

    return 0;
}

2. BFS

数据结构:queue
空间复杂度:O(2^h)
最短路

一般思路
queue    <-初始
while(队列不空) {
    t<-队头
    拓展队头
}
  • 走迷宫

    给定一个 n×m的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

    最初,有一个人位于左上角 (1,1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

    请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。

    数据保证 (1,1)(1,1) 处和 (n,m)处的数字为 0,且一定至少存在一条通路。

  int a[N][N], d[N][N];
  int n, m;
  int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
  
  int bfs() {
      queue<PII> q;
      memset(d, -1, sizeof d);
      q.push({0, 0});
      d[0][0] = 0;
      
      while (q.size()) {
          auto t = q.front();
          q.pop();
          for (int i = 0; i < 4; i ++ ) {
              int nx = t.first + dx[i], ny = t.second + dy[i];
              if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
                  continue;
              }
              if (a[nx][ny] == 1) {
                  continue;
              }
              if (d[nx][ny] != -1) {
                  continue;
              }
              // if (nx >= 0 && nx < n && ny >= 0 && ny < m && a[nx][ny] == 0 && d[nx][ny] == -1) {
                  d[nx][ny] = d[t.first][t.second]+ 1;
                  q.push({nx, ny});
              // }
              
          }
      }
      
      return d[n - 1][m - 1];
  }
  
  int main() {
      cin >> n >> m;
      for (int i = 0; i < n; i ++ ) {
          for (int j = 0; j < m; j ++ ) {
              cin >> a[i][j];
          }
      }
      
      cout << bfs() << endl;
      
      return 0;
  }
  • 八数码
 int bfs(string start) {
      string end = "12345678x";
      queue<string> q;
      unordered_map<string, int> d;
      
      q.push(start);
      d[start] = 0;
      int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
      while (q.size()) {
          auto t = q.front();
          q.pop();
          
          int dd = d[t];
          if (t == end) {
              return dd;
          }
          
          // 状态转移
          int k = t.find('x');
          int x = k / 3, y = k % 3;
          
          for (int i = 0; i < 4; i ++ ) {
              int nx = x + dx[i], ny = y + dy[i];
              if (nx >= 0 && nx < 3 && ny >= 0 && ny < 3) {
                  swap(t[k], t[nx * 3 + ny]);
                  if(!d.count(t)) {
                      d[t] = dd + 1; 
                      q.push(t); 
                  }
                  swap(t[k], t[nx * 3 + ny]);
              }
          }
      }
      return -1;
  }
  
  int main() {
      string start;
      for (int i = 0; i < 9; i ++ ) {
          char c;
          cin >> c;
          start += c;
      }
      cout << bfs(start) << endl;
      
      return 0;
  }
Flood Fill
  • 池塘计数

    农夫约翰有一片 N∗M的矩形土地。

    最近,由于降雨的原因,部分土地被水淹没了。

    现在用一个字符矩阵来表示他的土地。

    每个单元格内,如果包含雨水,则用”W”表示,如果不含雨水,则用”.”表示。

    现在,约翰想知道他的土地中形成了多少片池塘。

    每组相连的积水单元格集合可以看作是一片池塘。

    每个单元格视为与其上、下、左、右、左上、右上、左下、右下八个邻近单元格相连。

    请你输出共有多少片池塘,即矩阵中共有多少片相连的”W”块。

    输入格式

    第一行包含两个整数 N和 M。

    接下来 N行,每行包含 M个字符,字符为”W”或”.”,用以表示矩形土地的积水状况,字符之间没有空格。

    输出格式

    输出一个整数,表示池塘数目。

 char g[N][N];
  bool st[N][N];
  int n, m;
  int dx[8] = {-1, -1, -1, 0, 1, 1, 1, 0};
  int dy[8] = {-1, 0, 1, 1, 1, 0, -1, -1};
  
  void bfs(int x, int y) {
      queue<PII> q;
      q.push({x, y});
      st[x][y] = 1;
      while (q.size()) {
          auto t = q.front();
          q.pop();
          for (int i = 0; i < 8; i ++ ) {
              int nx = t.first+ dx[i], ny = t.second + dy[i];
              if (nx >= 0 && nx < n && ny >= 0 && ny < m && g[nx][ny] == 'W' && !st[nx][ny]) {
                  q.push({nx, ny});
                  st[nx][ny] = 1;
              }
              
          }
      }
  }
  
  int main() {
      scanf("%d%d", &n, &m);
      for (int i = 0; i < n; i ++ ) {
          scanf("%s", g[i]);
      }
      
      int res = 0;
      for (int i = 0; i < n; i ++ ) {
          for (int j = 0; j < m; j ++ ) {
              if (g[i][j] == 'W' && !st[i][j]) {
                  bfs(i, j);
                  res ++ ;
              }
          }
      }
      printf("%d", res);
      
      return 0;
  }
  • 城堡问题
    1   2   3   4   5   6   7  
   #############################
 1 #   |   #   |   #   |   |   #
   #####---#####---#---#####---#
 2 #   #   |   #   #   #   #   #
   #---#####---#####---#####---#
 3 #   |   |   #   #   #   #   #
   #---#########---#####---#---#
 4 #   #   |   |   |   |   #   #
   #############################
           (图 1)

   #  = Wall   
   |  = No wall
   -  = No wall

   方向:上北下南左西右东。

图1是一个城堡的地形图。

请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。

城堡被分割成 m∗n个方格区域,每个方格区域可以有0~4面墙。

注意:墙体厚度忽略不计。

输入格式

第一行包含两个整数 m 和 n,分别表示城堡南北方向的长度和东西方向的长度。

接下来 m行,每行包含 n个整数,每个整数都表示平面图对应位置的方块的墙的特征。

每个方块中墙的特征由数字 P来描述,我们用1表示西墙,2表示北墙,4表示东墙,8表示南墙,P为该方块包含墙的数字之和。

例如,如果一个方块的 P为3,则 3 = 1 + 2,该方块包含西墙和北墙。

城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。

输入的数据保证城堡至少有两个房间。

输出格式

共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。

int g[N][N];
int n, m;
bool st[N][N];
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};

int bfs(int x, int y) {
    queue<PII> q;
    q.push({x, y});
    st[x][y] = 1;
    int area = 0;
    
    while (q.size()) {
        auto t = q.front();
        q.pop();
        area ++ ;
        for (int i = 0; i < 4; i ++ ) {
            int nx = t.first + dx[i], ny = t.second + dy[i];
            if (nx >= 0 && nx < n && ny >= 0 && ny < m && !st[nx][ny] && !(g[t.first][t.second] >> i & 1)) {
                q.push({nx, ny});
                st[nx][ny] = 1;
            }
        }
    }
    
    return area;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < m; j ++ ) {
            cin >> g[i][j];
        }
    }
    
    int cnt = 0, area = 0;
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < m; j ++ ) {
            if (!st[i][j]) {
                area = max(area, bfs(i, j));
                cnt ++ ;
            }
        }
    }
    cout << cnt << endl << area << endl;
    
    return 0;
}
  • 山峰和山谷

FGD小朋友特别喜欢爬山,在爬山的时候他就在研究山峰和山谷。

为了能够对旅程有一个安排,他想知道山峰和山谷的数量。

给定一个地图,为FGD想要旅行的区域,地图被分为 n×n的网格,每个格子 (i,j)的高度 w(i,j)是给定的。

若两个格子有公共顶点,那么它们就是相邻的格子,如与 (i,j)相邻的格子有(i−1,j−1),(i−1,j),(i−1,j+1),(i,j−1),(i,j+1),(i+1,j−1),(i+1,j),(i+1,j+1)。

我们定义一个格子的集合 S为山峰(山谷)当且仅当:

  1. S 的所有格子都有相同的高度。
  2. S 的所有格子都连通。
  3. 对于 s 属于 S,与 s 相邻的 s不属于 S,都有 ws>ws′(山峰),或者 ws<ws′(山谷)。
  4. 如果周围不存在相邻区域,则同时将其视为山峰和山谷。

你的任务是,对于给定的地图,求出山峰和山谷的数量,如果所有格子都有相同的高度,那么整个地图即是山峰,又是山谷。

输入格式

第一行包含一个正整数 n,表示地图的大小。

接下来一个 n×n 的矩阵,表示地图上每个格子的高度 w。

输出格式

共一行,包含两个整数,表示山峰和山谷的数量。

int g[N][N];
bool st[N][N];
int n;
int dx[8] = {-1, -1, -1, 0, 1, 1, 1, 0};
int dy[8] = {-1, 0, 1, 1, 1, 0, -1, -1};

void bfs(int x, int y, bool& has_higher, bool& has_lower) {
    queue<PII> q;
    q.push({x, y});
    st[x][y] = 1;
    
    while (q.size()) {
        auto t = q.front();
        q.pop();
        
        for (int i = 0; i < 8; i ++ ) {
            int nx = t.first + dx[i], ny = t.second + dy[i];
            if (nx < 0 || nx >= n || ny < 0 || ny >= n) {
                continue;
            }
            if (g[nx][ny] != g[t.first][t.second]) {
                if (g[nx][ny] > g[t.first][t.second]) {
                    has_higher = true;
                } else {
                    has_lower = true;
                }
            } else if (!st[nx][ny]) {
                q.push({nx, ny});
                st[nx][ny] = 1;
            }
        }
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < n; j ++ ) {
            scanf("%d", &g[i][j]);
        }
    }
    
    int peak = 0, valley = 0;
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < n; j ++ ) {
            if (!st[i][j]) {
                bool has_higher = false, has_lower = false;
                bfs(i, j, has_higher, has_lower);
                if (!has_higher) {
                    peak ++ ;
                }
                if (!has_lower) {
                    valley ++ ;
                }
            }
        }
    }
    printf("%d %d", peak, valley);
    
    return 0;
}
最短路模型
  • 迷宫问题

给定一个 n×n的二维数组,如下所示:

int maze[5][5] = {

0, 1, 0, 0, 0,

0, 1, 0, 1, 0,

0, 0, 0, 0, 0,

0, 1, 1, 1, 0,

0, 0, 0, 1, 0,

};

它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。

数据保证至少存在一条从左上角走到右下角的路径。

输入格式

第一行包含整数 n。

接下来 n行,每行包含 n个整数 0 或 1,表示迷宫。

输出格式

输出从左上角到右下角的最短路线,如果答案不唯一,输出任意一条路径均可。

按顺序,每行输出一个路径中经过的单元格的坐标,左上角坐标为 (0,0)(0,0),右下角坐标为 (n−1,n−1)。

int n;
int g[N][N];
PII pre[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

void bfs(int x, int y) {
    memset(pre,  -1, sizeof pre);
    queue<PII> q;
    q.push({x, y});
    pre[x][y] = {0, 0};
    while (q.size()) {
        auto t = q.front();
        q.pop();
        for (int i = 0; i < 4; i ++ ) {
            int nx = t.first + dx[i], ny = t.second + dy[i];
            if (nx < 0  || nx >= n || ny < 0 || ny >= n) {
                continue;
            }
            if (g[nx][ny]) {
                continue;
            }
            if (pre[nx][ny].first != -1) {
                continue;
            }
            q.push({nx, ny});
            pre[nx][ny] =  t;
        }
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < n; j ++ ) {
            scanf("%d", &g[i][j]);
        }
    }
    
    bfs(n - 1, n - 1); // 因为要输出路径,所以倒序进行bfs可以在输出的时候直接正序输出
    PII end(0, 0);
    while (true) {
        printf("%d %d\n", end.first, end.second);
        if (end.first == n - 1 && end.second == n - 1) {
            break;
        }
        end = pre[end.first][end.second];
    }
    
    return 0;
}
  • 武士风度的牛

农民 John 有很多牛,他想交易其中一头被 Don 称为 The Knight 的牛。

这头牛有一个独一无二的超能力,在农场里像 Knight 一样地跳(就是我们熟悉的象棋中马的走法)。

虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个 x,y的坐标图来表示。

这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了 The Knight 的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。

现在你的任务是,确定 The Knight 要想吃到草,至少需要跳多少次。

The Knight 的位置用 K 来标记,障碍的位置用 * 来标记,草的位置用 H 来标记。

这里有一个地图的例子:

             11 | . . . . . . . . . .
             10 | . . . . * . . . . . 
              9 | . . . . . . . . . . 
              8 | . . . * . * . . . . 
              7 | . . . . . . . * . . 
              6 | . . * . . * . . . H 
              5 | * . . . . . . . . . 
              4 | . . . * . . . * . . 
              3 | . K . . . . . . . . 
              2 | . . . * . . . . . * 
              1 | . . * . . . . * . . 
              0 ----------------------
                                    1 
                0 1 2 3 4 5 6 7 8 9 0 

The Knight 可以按照下图中的 A,B,C,D…… 这条路径用 55 次跳到草的地方(有可能其它路线的长度也是 55):

             11 | . . . . . . . . . .
             10 | . . . . * . . . . .
              9 | . . . . . . . . . .
              8 | . . . * . * . . . .
              7 | . . . . . . . * . .
              6 | . . * . . * . . . F<
              5 | * . B . . . . . . .
              4 | . . . * C . . * E .
              3 | .>A . . . . D . . .
              2 | . . . * . . . . . *
              1 | . . * . . . . * . .
              0 ----------------------
                                    1
                0 1 2 3 4 5 6 7 8 9 0

注意: 数据保证一定有解。

输入格式

第 11 行: 两个数,表示农场的列数 C 和行数 R。

第 2…R+1行: 每行一个由 C个字符组成的字符串,共同描绘出牧场地图。

输出格式

一个整数,表示跳跃的最小次数。

int n, m;
char g[N][N];
int dist[N][N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};

int bfs() {
    int x, y;
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < m; j ++ ) {
            if (g[i][j] == 'K') {
                x = i, y = j;
            }
        }
    }
    
    queue<PII> q;
    q.push({x, y});
    memset(dist, -1, sizeof dist);
    dist[x][y] = 0;
    while (q.size()) {
        auto t = q.front();
        q.pop();
        for (int i = 0; i < 8; i ++ ) {
            int nx = t.first + dx[i], ny = t.second + dy[i]; 
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
                continue;
            }
            if (g[nx][ny] == '*') {
                continue;
            }
            if (dist[nx][ny] != -1) {
                continue;
            }
            if (g[nx][ny] == 'H') {
                return dist[t.first][t.second] + 1;
            }
            
            dist[nx][ny] = dist[t.first][t.second] + 1;
            q.push({nx, ny});
        }
    }
    return -1;    
}

int main() {
    cin >> m >> n;
    for (int i = 0; i < n; i ++ ) {
        cin >> g[i];
    }
    
    cout << bfs() << endl;
    
    return 0;
}
  • 抓住那头牛

农夫知道一头牛的位置,想要抓住它。

农夫和牛都位于数轴上,农夫起始位于点 N,牛位于点 K。

农夫有两种移动方式:

  1. 从 X移动到 X−1或 X+1,每次移动花费一分钟
  2. 从 X移动到 2∗X,每次移动花费一分钟

假设牛没有意识到农夫的行动,站在原地不动。

农夫最少要花多少时间才能抓住牛?

输入格式

共一行,包含两个整数N和K。

输出格式

输出一个整数,表示抓到牛所花费的最少时间。

int dist[N];
int n, k;

int bfs() {
    queue<int> q;
    memset(dist, -1, sizeof dist);
    dist[n] = 0;
    q.push({n});
    while (q.size()) {
        int t = q.front();
        q.pop();
        if (t == k) {
            return dist[k];
        }
        if (t + 1 < N && dist[t + 1] == -1) {
            dist[t + 1] = dist[t] + 1;
            q.push({t + 1});
        }
        if (t - 1 >= 0 && dist[t - 1] == -1) {
            dist[t - 1] = dist[t] + 1;
            q.push({t - 1});
        }
        if (t * 2 < N && dist[t * 2] == -1) {
            dist[t * 2] = dist[t] + 1;
            q.push({t * 2});
        }
    }
    
    return -1;
}

int main() {
    cin >> n >> k;
    cout << bfs() << endl;
    
    return 0;
}

3. 树与图的深度优先遍历

模板代码

vector<int> v[N];
int n, m;
bool st[N];

// 以u为根的子树中点的数量
void dfs(int u) {
    st[u] = 1; // 标记一下,已经被搜过了

    int sum = 1, res = 0; // sum是当前子树大小,res是删掉当前点每一个连通块大小的最大值
    for (int i = 0; i < v[u].size(); i ++ ) { // 遍历u的出边
        int k = v[u][i]; // 编号
        if (!st[k]) {
            // 具体思路
        }
    }
}

int main() {
    cin >> n;
    for (int i = 0; i < n - 1; i ++ ) {
        int a, b;
        cin >> a >> b;
        v[b].push_back(a), v[a].push_back(b); // 存图,具体情况具体分析
    }
    dfs(1);

    return 0;
}

4. 树与图的广度优先遍历

queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size()) {
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!st[j]) {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}
  • 图中点的层次
int n, m;
int h[N], e[N], ne[N], idx;//邻接表
int d[N];//d是距离

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int bfs() {
    queue<int> q;
    memset(d, -1, sizeof d);//初始化距离
    
    d[1] = 0;//最开始只有第一个点被遍历过
    q.push(1);
    
    while(q.size()) {
        int t = q.front();//取队头
        q.pop();//弹出队头元素
        
        for(int i = h[t]; i != -1; i = ne[i]) { // 扩展当前点
            int j = e[i]; // j表示当前点
            if(d[j] == -1) { // j没有遍历过
                d[j] = d[t] + 1;
                q.push(j);
            }
        }
    }
    
    return d[n];
}

int main() {
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    for(int i = 0; i < m; i ++) {
        int a, b;
        cin >> a >> b;;
        add(a, b);
    }
    cout << bfs() << endl;
    
    return 0;
}

5. 拓扑排序

时间复杂度 O(n+m), n表示点数,m表示边数

bool topsort() {
    int hh = 0, tt = -1;

    // d[i] 存储点i的入度
    for (int i = 1; i <= n; i ++ )
        if (!d[i])
            q[ ++ tt] = i;

    while (hh <= tt) {
        int t = q[hh ++ ];
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (-- d[j] == 0)
                q[ ++ tt] = j;
        }
    }

    // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
    return tt == n - 1;
}

6. Dijkstra

朴素dijkstra算法
时间复杂是 O(n^2+m), n表示点数,m表示边数

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ ) {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

堆优化版dijkstra —— 模板题 AcWing 850. Dijkstra求最短路 II
时间复杂度 O(mlogn), n表示点数,m表示边数

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    while (heap.size()) {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > distance + w[i]) {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

7.Bellman-Ford算法

时间复杂度 O(nm), n表示点数,m表示边数
注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。

int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge {    // 边,a表示出点,b表示入点,w表示边的权重
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < m; j ++ ) {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

8. spfa

队列优化的Bellman-Ford算法
时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n表示点数,m表示边数
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size()) {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                if (!st[j]) {     // 如果队列中已存在j,则不需要将j重复插入
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
spfa判断图中是否存在负环
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中

// 如果存在负环,则返回true,否则返回false。
bool spfa() {
    // 不需要初始化dist数组
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    for (int i = 1; i <= n; i ++ ) {
        q.push(i);
        st[i] = true;
    }

    while (q.size()) {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

9. Floyd

时间复杂度是 O(n^3), n表示点数
初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd() {
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

10. Prim

朴素版prim算法

时间复杂度是 O(n^2+m),n表示点数,m表示边数
int n;      // n表示点数
int g[N][N];        // 邻接矩阵,存储所有边
int dist[N];        // 存储其他点到当前最小生成树的距离
bool st[N];     // 存储每个点是否已经在生成树中


// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim() {
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 0; i < n; i ++ ) {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        if (i && dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

11. Kruskal

时间复杂度是 O(mlogm), n表示点数,m表示边数
int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组

struct Edge {     // 存储边
    int a, b, w;

    bool operator< (const Edge &W)const {
        return w < W.w;
    }
}edges[M];

int find(int x) {     // 并查集核心操作
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal() {
    sort(edges, edges + m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ ) {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b) {     // 如果两个连通块不连通,则将这两个连通块合并
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

12. 染色法判定二分图

时间复杂度是 O(n+m), n表示点数,m表示边数
int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色

// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c) {
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (color[j] == -1) {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;
    }

    return true;
}

bool check() {
    memset(color, -1, sizeof color);
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
        if (color[i] == -1)
            if (!dfs(i, 0)) {
                flag = false;
                break;
            }
    return flag;
}

13. 匈牙利算法

时间复杂度是 O(nm), n表示点数,m表示边数
int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过

bool find(int x) {
    for (int i = h[x]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!st[j]) {
            st[j] = true;
            if (match[j] == 0 || find(match[j])) {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}

// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ ) {
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值