【算法筆記】《labuladong 的算法小抄》第 1 章:核心套路篇之回溯算法與 BFS 算法

1.3. 回溯算法

  • 回溯問題決策樹 的遍歷過程挂洛,純暴力枚舉
    1. 路徑已做出 的選擇
    2. 選擇列表當(dāng)前能做 的選擇
    3. 結(jié)束條件無法再做 選擇的條件
result = []
def backtrack( 路徑, 選擇列表 ):
  if 滿足結(jié)束條件:
    result.add( 路徑 )
    return

  for 選擇 in 選擇列表:
    做選擇
    backtrack( 路徑, 選擇列表 )
    撤銷選擇


1.3.1. 全排列問題

n 個不重復(fù)的數(shù)的全排列共有 n! 個

  1. 路徑:[2]
  2. 選擇列表:[1, 3]
  3. 結(jié)束條件:選擇列表為空
全排列問題的回溯樹
  • 時間復(fù)雜度 O(n * n!),遞歸總次數(shù) * 每次遞歸中的操作次數(shù)
  • 空間復(fù)雜度 O(n),遞歸深度 * 每次遞歸的輔助空間
// 全排列鏈表
List<List<Integer>> res = new LinkedList<>();

/* 主函數(shù)宪祥,輸入一組不重復(fù)的數(shù)字,返回它們的全排列 */
List<List<Integer>> permute(int[] nums) {
  // 記錄 “路徑”
  LinkedList<Interger> track = new LinkedList<>();
  backtrack(nums, track);
  return res;
}

// 路徑:記錄在 track 中
// 選擇列表:nums 中不存在于 track 中的元素
// 結(jié)束條件:nums 中元素全部都在 track 中
void backtrack(int[] nums, LinkedList<Integer> track) {
  // 觸發(fā)結(jié)束條件
  if (track.size() == nums.length) {
    res.add(new LinkedList(track));
    return;
  }

  for (int i = 0; i < nums.length; i++) {
    // 排除不合法的選擇
    if (track.contains(nums[i])) {
      continue;
    }
    // 做選擇
    track.add(nums[i]);
    // 進入下一層決策樹
    backtrack(nums, track);
    // 撤銷選擇
    track.removeLast();
  }
}


1.3.2. N 皇后問題

N × N 的棋盤仗处,放置 N 個皇后胳喷,使它們不能從八個方向互相攻擊

List<List<String>> res = new LinkedList<>();

/* 輸入棋盤邊長 n,返回所有合法的放置方法*/
List<List<String>> solveNQueens(int n) {
  // '.' 表示空席吴,'Q' 表示皇后赌结,初始化空棋盤
  List<String> board = new LinkedList<>();
  for (int i = 0; i < n; i++) {
    char[] row = new char[n];
    Arrays.fill(row, '.');
    board.add(new String(row));
  }
  backtrack(board, 0);
  return res;
}

// 路徑:board 中小于 row 的行已經(jīng)成功放置皇后
// 選擇列表:第 row 行所有列都是放置皇后的選擇
// 結(jié)束條件:row 大于 board 的最后一行捞蛋,說明棋盤已滿
void backtrack(List<String> board, int row) {
  // 觸發(fā)結(jié)束條件
  if (board.size() == row) {
    res.add(new LinkedList(board));
    return;
  }

  int n = board.get(row).length();
  for (int col = 0; col < n; col++) {
    // 排除不合法選擇
    if (!isValid(board, row, col)) {
      continue;
    }
    // 做選擇
    char[] arr = new char[n];
    Arrays.fill(arr, '.');
    arr[col] = 'Q';
    board.set(row, new String(arr));
    // 進入下一行決策
    backtrack(board, row + 1);
    // 撤銷選擇
    arr[col] = '.';
    board.set(row, new String(arr));
  }
}

/* 檢查 board.get(row).charAt(col) 處是否可以放置皇后 */
boolean isValid(List<String> board, int row, int col) {
  int n = board.get(row).length();
  // 檢查列中是否有皇后沖突
  for (int i = 0; i < row; i++) {
    if (board.get(i).charAt(col) == 'Q') {
      return false;
    }
  }

  // 檢查左上方是否有皇后沖突
  for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
    if (board.get(i).charAt(j) == 'Q') {
      return false;
    }
  }

  // 檢查右上方是否有皇后沖突
  for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
    if (board.get(i).charAt(j) == 'Q') {
      return false;
    }
  }

  return true;
}
  • 注意此算法每次遞歸都需要開辟新數(shù)組,導(dǎo)致空間復(fù)雜度大柬姚,原因是 String 無法直接修改元素


1.4. BFS 算法

  • DFS 算法:深度優(yōu)先
    • 利用 遞歸拟杉,遍歷決策樹
    • 全排列 / 可行路徑
    • 時間復(fù)雜度大
  • BFS 算法:廣度優(yōu)先
    • 利用 隊列的起點到終點
    • 最短路徑
    • 空間復(fù)雜度大


  • 核心數(shù)據(jù)結(jié)構(gòu):Queue
  • 避免走回頭路(圖):visited
// 計算從起點 start 到終點 target 的最短距離
int BFS( Node start, Node target) {
  Queue<Node> q; // 核心數(shù)據(jù)結(jié)構(gòu)
  Set<Node> visited; // 避免走回頭路

  q.offer(start); // 將起點加入隊列
  visited.add(start);
  int step = 0; // 記錄步數(shù)

  while (q not empty) {
    int sz = q.size();
    /* 將當(dāng)前隊列中的所有節(jié)點向四周擴散 */
    for (int i = 0; i < sz; i++) {
      Node cur = q.poll();
      /* 判斷是否到達終點 */
      if (cur is target) {
        return step;
      }
      /* 若未訪問過量承,則將 cur 的相鄰節(jié)點加入隊列 */
      for (Node x : cur.adj()) {
        if (x not in visited) {
          q.offer(x);
          visited.add(x);
        }
      }
    }
    /* 更新步數(shù) */
    step++;
  }
}


1.4.1. 二叉樹的最小高度

輸入一棵二叉樹搬设,計算從根節(jié)點到葉子節(jié)點的最短距離

葉子節(jié)點:左右子節(jié)點都為空

if (cur.left == null && cur.right == null)

int minDepth(TreeNode root) {
  if (root == null) return 0;
  Queue<TreeNode> q = new LinkedList<>();
  q.offer(root);
  int depth = 1;

  while (!q.isEmpty()) {
    int sz = q.size();
    /* 將當(dāng)前隊列中的所有節(jié)點向四周擴散 */
    for (int i = 0; i < sz; i++) {
      TreeNode cur = q.poll();
      /* 判斷是否到達終點,即葉子節(jié)點 */
      if (cur.left == null && cur.right == null) {
        return depth;
      }
      /* 將 cur 的子節(jié)點加入隊列 */
      if (cur.left != null) {
        q.offer(cur.left);
      }
      if (cur.right != null) {
        q.offer(cur.right);
      }
    }
    /* 更新高度 */
    depth++;
  }
  return depth;
}


1.4.2. 解開密碼鎖的最小次數(shù)

帶有四個圓形撥輪的轉(zhuǎn)盤鎖撕捍,每次旋轉(zhuǎn)只能將一個撥輪旋轉(zhuǎn)一次拿穴,計算從初始狀態(tài) "0000" 撥出 target 的最少次數(shù),同時避免撥出 deadends

int openLock(String[] deadends, String[] target) {
  // 記錄需要避免的死亡密碼
  Set<String> deads = new HashSet<>();
  for (String s : deadends) {
    deads.add(s);
  }
  // 記錄已經(jīng)窮舉過的密碼忧风,避免走回頭路
  Set<String> visited = new HashSet<>();
  Queue<String> q = new LinkedList<>();
  // 從初始狀態(tài)開始啟動 BFS 算法
  int step = 0;
  q.offer("0000");
  visited.add("0000");

  while (!q.isEmpty()) {
    int sz = q.size();
    // 將當(dāng)前隊列中的所有節(jié)點向周圍擴散
    for (int i = 0; i < sz; i++) {
      String cur = q.poll();
      // 判斷密碼是否是死亡密碼
      if (deads.contains(cur)) {
        continue;
      }
      // 判斷密碼是否到達終點
      if (cur.equals(target)) {
        return step;
      }

      // 將四個撥輪未遍歷的相鄰節(jié)點加入隊列
      for (int j = 0; j < 4; j++) {
        String up = plusOne(cur, j);
        if (!visited.contains(up)) {
          q.offer(up);
          visited.add(up);
        }
        String down = minusOne(cur, j);
        if (!visited.contains(down)) {
          q.offer(down);
          visited.add(down);
        }
      }
    }
    // 增加步數(shù)
    step++;
  }
  // 找不到 target 密碼默色,返回 -1
  return -1;
}

// 將 s[j] 向上撥動一次
String plusOne(String s, int j) {
  char[] ch = s.toCharArray();
  if (ch[j] == '9') {
    ch[j] = '0';
  }
  else {
    ch[j] += 1;
  }
  return new String(ch);
}

// 將 s[j] 向下?lián)軇右淮?String minusOne(String s, int j) {
  char[] ch = s.toCharArray();
  if (ch[j] == '0') {
    ch[j] = '9';
  }
  else {
    ch[j] -= 1;
  }
  return new String(ch);
}

注意:遍歷與否在加入相鄰節(jié)點時檢查

  • 時間復(fù)雜度 O(N * AN),遍歷全部密碼需要 O(AN)狮腿,每個密碼撥動每一位需要 O(N)
  • 空間復(fù)雜度 O(AN)
  • A 為數(shù)字個數(shù)腿宰,N 為密碼位數(shù)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蚤霞,隨后出現(xiàn)的幾起案子酗失,更是在濱河造成了極大的恐慌,老刑警劉巖昧绣,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件规肴,死亡現(xiàn)場離奇詭異,居然都是意外死亡夜畴,警方通過查閱死者的電腦和手機拖刃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贪绘,“玉大人兑牡,你說我怎么就攤上這事∷肮啵” “怎么了均函?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長菱涤。 經(jīng)常有香客問我苞也,道長,這世上最難降的妖魔是什么粘秆? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任如迟,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘殷勘。我一直安慰自己此再,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布玲销。 她就那樣靜靜地躺著输拇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贤斜。 梳的紋絲不亂的頭發(fā)上淳附,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音蠢古,去河邊找鬼。 笑死别凹,一個胖子當(dāng)著我的面吹牛草讶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炉菲,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼堕战,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拍霜?” 一聲冷哼從身側(cè)響起嘱丢,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祠饺,沒想到半個月后越驻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡道偷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年缀旁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勺鸦。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡并巍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出换途,到底是詐尸還是另有隱情懊渡,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布军拟,位于F島的核電站剃执,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吻谋。R本人自食惡果不足惜忠蝗,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漓拾。 院中可真熱鬧阁最,春花似錦戒祠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至配阵,卻和暖如春馏颂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棋傍。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工救拉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘫拣。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓亿絮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親麸拄。 傳聞我的和親對象是個殘疾皇子派昧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容