二分图

785. 判断二分图

  • 利用队列和广度优先搜索,对未染色的节点进行染色,并且检查是否有颜色相同的相邻节点存在。
  • 用0代表未检查的节点,用1和2表示两种不同的颜色。

    1. class Solution {
    2. public:
    3. bool isBipartite(vector<vector<int>>& graph) {
    4. int n = graph.size();//代表有多少点
    5. vector<int> color(n, 0);//每个点都有一个颜色,0代表没有遍历到,1和2代表两种颜色。
    6. queue<int> q;
    7. for(int i = 0; i < n ; i++){
    8. if(color[i] == 0){
    9. q.push(i);
    10. color[i] = 1;
    11. }
    12. while(!q.empty()){
    13. int node = q.front();//node是当前点
    14. q.pop();
    15. for(int a : graph[node]){//和当前节点相连的节点都入队
    16. if(color[a] == 0){//如果a还没有遍历过
    17. q.push(a);
    18. color[a] = color[node]== 1 ? 2:1;
    19. }
    20. else if(color[a] == color[node]){
    21. return false;
    22. }
    23. }
    24. }
    25. }
    26. return true;
    27. }
    28. };

    拓扑排序

    给定一个包含n个节点的有向图G,我们给出它的节点编号的一种排列,如果满足:
    对于图G的任意一条有向边(u,v),u在排列中都出现在v的前面,那么称该排列是图G的拓扑排列。
    根据上述定义可以得出两个结论:

  • 如果图G中存在环,那么图G不存在拓扑排序。这是因为假设途中存在环x1,x2,⋯,xn,x1,那么x1在排列中必须出现在xn的前面,但xn同时也必须出现在x1的前面,自相矛盾。

  • 如果图G是有向无环图,那么它的拓扑排序可能不止一种。举一个极端例子,如果图G值包含n个节点却没有边,那么任意一种编号的排列都可以作为拓扑排序。


207. 课程表(经典拓扑排序)

  • 通过拓扑排序来看是否能通过所有课程
  • 我们将每一门课看成一个节点
  • 如果想要学习A之前必须完成课程B,那么我们从B到A连接一条有向边。这样以来,在拓扑排序中,B一定要出现在A的前面。
  • edges为出度的集合,代表这个课程是多少个课程的前置。indeg为入度,代表这个课程需要多少个前置。
  1. class Solution {
  2. private:
  3. vector<vector<int>> edges;
  4. vector<int> indeg;
  5. public:
  6. bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
  7. edges.resize(numCourses);
  8. indeg.resize(numCourses);
  9. for (const auto& info: prerequisites) {
  10. edges[info[1]].push_back(info[0]);
  11. ++indeg[info[0]];
  12. }
  13. queue<int> q;
  14. for (int i = 0; i < numCourses; ++i) {
  15. if (indeg[i] == 0) {
  16. q.push(i);
  17. }
  18. }
  19. int visited = 0;
  20. while (!q.empty()) {
  21. ++visited;
  22. int u = q.front();
  23. q.pop();
  24. for (int v: edges[u]) {
  25. --indeg[v];
  26. if (indeg[v] == 0) {
  27. q.push(v);
  28. }
  29. }
  30. }
  31. return visited == numCourses;
  32. }
  33. };

210. 课程表 II

  • 这一题与上一题不同的是,返回的是上课程的顺序。

    1. class Solution {
    2. public:
    3. vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
    4. vector<vector<int>> graph(numCourses,vector<int>());//建图
    5. vector<int> indegree(numCourses,0);//入度
    6. queue<int> q;
    7. vector<int> res;
    8. for(int i = 0; i < prerequisites.size(); i++){
    9. graph[prerequisites[i][1]].push_back(prerequisites[i][0]);
    10. ++indegree[prerequisites[i][0]];
    11. }
    12. for(int i = 0; i < indegree.size(); i++){
    13. if(!indegree[i])//入度为零的先入队
    14. q.push(i);
    15. }
    16. while(!q.empty()){
    17. int u = q.front();
    18. res.push_back(u);
    19. q.pop();
    20. for(auto v : graph[u]){
    21. indegree[v]--;
    22. if(!indegree[v]){//入度为零入队
    23. q.push(v);
    24. }
    25. }
    26. }
    27. for(int i = 0; i < indegree.size(); i++){
    28. if(indegree[i])//如果存在课程入度不为0,则直接返回空vector
    29. return vector<int>();
    30. }
    31. return res;
    32. }
    33. };

    310. 最小高度树(拓扑排序变形)

  • 本题其实就是拓扑排序的变式。就是不断缩小图,直至剩下1、2个点,return。

  • 本题为无向图,所以删除的点是入度为1的点
  • 本题不能一个个的删除入度为1的点,而应该在一个循环中,一次性删除入度为1的点,使得以同样的速度缩小图,直至剩下的点。

证明:其实这种方法只能正针对有树特征的图(无环结构),同时答案至多有两个解。 答案至多有两个解是应该可以证明的,下面是我不严谨的证法:这个图是可以进行等效剪枝的(去掉距离相同或者更短的枝干),最终可以将图化简为一条链,链长度为偶数则有两个解,链长度为奇数则有一个解。

  1. class Solution {
  2. private:
  3. // 找最小高度,最短路径想到BFS
  4. // 两端烧香求中点
  5. public:
  6. vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
  7. if (n == 1) return {0};
  8. vector<int> res;
  9. // 每个节点的度数
  10. vector<int> degree(n);
  11. // 建立无向邻接图
  12. vector<vector<int>> map(n);
  13. for (int i = 0; i < edges.size(); i++) {
  14. int v1 = edges[i][0];
  15. int v2 = edges[i][1];
  16. degree[v1]++;
  17. degree[v2]++;
  18. map[v1].push_back(v2);
  19. map[v2].push_back(v1);
  20. }
  21. // 把度为1的节点入队
  22. queue<int> q;
  23. for (int i = 0; i < n; i++) {
  24. if (degree[i] == 1)
  25. q.push(i);
  26. }
  27. // BFS
  28. while (!q.empty()) {
  29. // 清理当前层的节点
  30. res.clear();
  31. int size = q.size();
  32. while (size--) {
  33. int cur = q.front();
  34. q.pop();
  35. res.push_back(cur);
  36. // 减小对应度数
  37. degree[cur]--;
  38. for (auto i : map[cur]) {
  39. degree[i]--;
  40. if (degree[i] == 1) {
  41. q.push(i);
  42. }
  43. }
  44. }
  45. }
  46. return res;
  47. }
  48. };

279. 完全平方数(bfs+建图)

建图:

  • 根据bfs一层一层建图
  • 记忆化来剪枝

    1. class Solution {
    2. public:
    3. int numSquares(int n) {
    4. //图论,从n到0,每个数字表示一个节点,如果两个数字之间此昂差一个完全平方数,则连接一条边,求最短路径(无权图))
    5. assert(n>0);
    6. queue<int> q;//分别存储n和需要走几步到达该数
    7. q.push(n);
    8. int res = 0;
    9. vector<bool> visited(n+1,false);//排除冗余的节点
    10. visited[n]=true;
    11. while(!q.empty()){
    12. int size = q.size();
    13. while(size--){
    14. int num = q.front();
    15. q.pop();
    16. if(num == 0) return res;
    17. for(int i = 1;num-i*i>=0; i++){
    18. if(!visited[num-i*i]){
    19. visited[num-i*i]=true;
    20. q.push(num-i*i);
    21. }
    22. }
    23. }
    24. res++;
    25. }
    26. throw invalid_argument("No Solution.");
    27. }
    28. };

    动规:

    根据子集与子集之间的关系来计算父集。
    状态转移方程:
    image.png

    1. class Solution {
    2. public:
    3. int numSquares(int n) {
    4. vector<int> dp(n+1,INT_MAX);
    5. dp[0]=0;
    6. for(int i =1;i<=n;i++){
    7. for(int j = 1;j*j<=i;j++){
    8. dp[i] = min(dp[i],dp[i-j*j]+1);
    9. }
    10. }
    11. return dp[n];
    12. }
    13. };

    剑指 Offer II 114. 外星文字典

    1. class Solution {
    2. public:
    3. string alienOrder(vector<string>& words) {
    4. unordered_map<char, unordered_set<char>> graph;//这里应该是每个字母的后面的字母。
    5. unordered_map<char, int> inDegress;
    6. // 建图
    7. for (auto& word : words) {
    8. for (auto& ch : word) {
    9. if (!graph.count(ch)) {
    10. graph[ch] = {};
    11. }
    12. if (!inDegress.count(ch)) {
    13. inDegress[ch] = 0;
    14. }
    15. }
    16. }
    17. // 计算邻接表和入度
    18. for (int i = 1; i < words.size(); ++i) {//通过对比相邻的两个字符串来计算入度。
    19. int j = 0;
    20. int len = min(words[i - 1].size(), words[i].size());
    21. for (; j < len; ++j) {
    22. char ch1 = words[i - 1][j];
    23. char ch2 = words[i][j];
    24. if (ch1 != ch2) {//出现不等直接break,因为ch1排序在ch2之前。
    25. if (!graph[ch1].count(ch2)) {
    26. graph[ch1].insert(ch2);
    27. inDegress[ch2]++;
    28. }
    29. break;
    30. }
    31. }
    32. // 特殊判断
    33. //如果后面的单词是前面单词的前缀,并且后面的单词小于前面的单词的长度。直接返回空即可
    34. if (j == len && words[i - 1].size() > words[i].size()) {//如果是无效字母顺序
    35. return {};
    36. }
    37. }
    38. string ret{""};
    39. queue<char> que;
    40. // 入度为 0 的点,先入队
    41. for (auto& d : inDegress) {
    42. if (d.second == 0) {
    43. que.push(d.first);
    44. }
    45. }
    46. // BFS,进行搜索
    47. while (!que.empty()) {
    48. char ch = que.front();
    49. que.pop();
    50. ret.push_back(ch);
    51. for (auto& c : graph[ch]) {
    52. inDegress[c]--;
    53. if (inDegress[c] == 0) {
    54. que.push(c);
    55. }
    56. }
    57. }
    58. if (ret.size() != inDegress.size()) {//存在字符的入度无法为0,非法
    59. return "";
    60. }
    61. return ret;
    62. }
    63. };

    433. 最小基因变化(双向bfs+常看)

    image.png
    image.png

  • 这里分别标记从前往后为1,从后往前为2,如果标记为3的话就代表前和后都遍历到了。就是我们要的答案。

    1. class Solution {
    2. public:
    3. // 解法2:双向bfs
    4. int minMutation(string start, string end, vector<string>& bank) {
    5. // 1:建立hashmap表,顺便去掉重复元素
    6. unordered_map<string,int> mp;
    7. for(const auto& b:bank)mp[b]=0;
    8. // 2:排除极端情况,end不在基因库中
    9. if(mp.count(end)==0)return -1;
    10. // 3:bfs的初始化工作
    11. queue<string> q1({start}),q2({end});// 前向队列,后向队列
    12. int step=0;
    13. const char table[4]={'A','C','G','T'};// 基因的字符
    14. // 或1表示前向队列由前往后遍历,或2表示后向队列由后向前遍历,每次我们选用较小的队列进行遍历
    15. for(mp[start]|=1,mp[end]|=2;q1.size()&&q2.size();++step)//每遍历完一次,步长+1
    16. {
    17. bool first=q1.size()<q2.size();
    18. //注意后面使用引用来写的,这很重要!
    19. queue<string> &q=first?q1:q2;// 选择较小的队列进行遍历节约时间,两个队列轮询。
    20. int flag=first?1:2;// 此次遍历的方式
    21. for(int n=q.size();n--;q.pop()){
    22. string& temp=q.front();
    23. if(mp[temp]==3)return step;// 两个队列碰头,返回步长,这里是即与1或了,也与2或了。就代表两个方向出现了公共点。
    24. for(int i=0;i<temp.size();++i){//对字符串中的每一个字符,都可以转换成table中的别的字符。
    25. for(int j=0;j<4;++j){
    26. string s=temp;
    27. if(s[i]==table[j]) continue;// 重复字符,跳出循环,寻找下一个字符
    28. s[i]=table[j];
    29. if(mp.count(s)==0||mp[s]==flag) continue;// 该单词不在基因库中,或该单词已经被遍历过了,跳出循环,寻找下一个单词
    30. mp[s]|=flag;// 标记该单词已经被遍历过了
    31. q.push(s);
    32. }
    33. }
    34. }
    35. }
    36. return -1;
    37. }
    38. };