lcBackTrack - 回溯

0 回溯

回溯的思想在于回这个字,以最经典的八皇后作为例子:

1
2
3
4
5
backTrack(当前层数,棋盘):
for: 当前层的每个位置
放置一个棋子到这个位置
backTrack(下一层, 棋盘) // 深入到下一层
取消将棋子放到这个位置 // 回溯,以便于尝试当前层的下一个位置

当然,回溯也有当前搜索的解空间的情况,比如对于棋盘就是目前已经放置的所有的位置,可以使用记忆化搜锁
下面的题目中0698就使用了

1 例题:

0698canPartitionKSubsets 可以划分为k个等和子集

1 题目

https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/

2 解题思路

  • 1 解题思路:

    • 1.1 首先,我们之前的回溯都是最多一个搜索目标,比如8皇后,那就是一个棋盘作为搜索目标,比如划分成2个等和子集,那就是搜索一个子集的值为sum/2,那么这种多目标如何考虑?其实就是,这样想,多个等和子集对吧?那不就是对于数组的每一个数组,在0到k-1号集合里面嘛,这样去回溯就好了
    • 1.2 那么需要写成: 每一个数字在每一个桶里面的情况吗?不需要,为啥?因为对于第i个数字,我们会尝试每一个桶,写成双层for循环智慧带来多余的计算:
    • 1.3 什么时候能进入下一层?当前桶能够放下没被用过的最小的数字,那么没被用过的数字是否需要标记,不需要,为什么,因为我们回溯的时候,每个数字就是下一层,然后每一层不过有4个选择罢了
    • 1.4 考虑使用记忆化加速?
      • 比如:当nums[i]放入通bucket[buc],失败了,那么后面所有桶值为bucket[buc],那么就不再去搜索就行
      • 那么需要注意:这是考虑在所有nums[0 : i-1]都相同的情况下才能用这个记忆,一旦第i号所有搜索都失败了,那么我们需要将memo[i]清除,以便后续操作
  • 2 1.2的时机例子如下:典型的多余的for循环:1取到1,2取到2,那么当最外层的2取到2,1又取到1,那不就是重复计算了嘛

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // for every number, try bucket from 0 to k-1
    void backTrack(vector<int>& nums, vector<int>& buckets, int tar) {
    if(fin) {
    return ;
    }

    bool tmp = true;
    for(auto& buc : buckets) {
    tmp = tmp && (buc == tar);
    }
    if(tmp) {
    fin = true;
    }

    for(int i = 0; i < nums.size(); ++i) {
    if(!used[i]) {
    for(int buc = 0; buc < buckets.size(); ++buc) {
    buckets[buc] += nums[i]; // put it into buc
    used[i] = true;
    if(buckets[buc] <= tar) {
    backTrack(nums, buckets, tar);
    }
    buckets[buc] -= nums[i];
    used[i] = false;
    }
    }
    }

    }

标准的带有返回值的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Solution {
public:
vector<int> used;
bool canPartitionKSubsets(vector<int>& nums, int k) {
vector<int> buckets(k, 0);
used.resize(nums.size(), false);

sort(nums.rbegin(), nums.rend());

int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum % k != 0) {
return false;
}

int tar = sum / k;
if(nums[0] > tar) {
return 0;
}

vector<unordered_set<int>> memo(nums.size()); // for node[i], memo[i][j]means: node[i] put into a bucket with value j will failed

return backTrack(nums, buckets, 0, tar, memo);
}

// for every number, try bucket from 0 to k-1
bool backTrack(vector<int>& nums, vector<int>& buckets, int st, int tar, vector<unordered_set<int>>& memo) {

// 所有数字都放进去了,成功了
if(st == nums.size()) {
return true;
}

for(int buc = 0; buc < buckets.size(); ++buc) {
if(0 == memo[st].count(buckets[buc])) {
// if(buckets[i]) {
buckets[buc] += nums[st]; // put it into buc
// used[st] = true;
if(buckets[buc] == tar || (tar - buckets[buc] >= nums.back())) {
// cout << "to buc: " << buc << endl;
if(backTrack(nums, buckets, st + 1, tar, memo)) {
return true; // 提前返回,因为已经找到一个结果
}
}
buckets[buc] -= nums[st];

// 如果nums[st] 放入 buckets[buc]失败了,那么后面值相同的桶都会失败,就不用放了
memo[st].insert(buckets[buc]);
}
}

// 说明放错桶的不是st,而是nums[0 : st-1]中的某个,于是把memo清除掉
memo[st].clear();

return false;
}
}

我写的回溯不喜欢带返回值,后面是带有返回值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class Solution {
public:
vector<int> used;
bool fin = false;
bool canPartitionKSubsets(vector<int>& nums, int k) {
vector<int> buckets(k, 0);
used.resize(nums.size(), false);

sort(nums.rbegin(), nums.rend());

int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum % k != 0) {
return false;
}

int tar = sum / k;
if(nums[0] > tar) {
return 0;
}

int useCnt = 0;

vector<unordered_set<int>> memo(nums.size());
backTrack(nums, buckets, 0, tar, useCnt, memo);

return fin;
}

// for every number, try bucket from 0 to k-1
void backTrack(vector<int>& nums, vector<int>& buckets, int st, int tar, int& useCnt, vector<unordered_set<int>>& memo) {
if(fin) {
return ;
}

if(useCnt == nums.size()) {
bool tmp = true;
for(auto& buc : buckets) {
tmp = tmp && (buc == tar);
}
if(tmp) {
fin = true;
return;
}
}

if(useCnt == nums.size()) {
return; // return true就行
}


for(int buc = 0; buc < buckets.size(); ++buc) {
if(0 == memo[st].count(buckets[buc])) {
buckets[buc] += nums[st]; // put it into buc
++useCnt;
if(buckets[buc] == tar || (tar - buckets[buc] >= nums.back())) {
// cout << "to buc: " << buc << endl;
backTrack(nums, buckets, st + 1, tar, useCnt, memo);
}

buckets[buc] -= nums[st];

// 如果nums[st] 放入 buckets[buc]失败了,那么后面值相同的桶都会失败,就不用放了
memo[st].insert(buckets[buc]);

--useCnt;
}
}

// 说明放错桶的不是st,而是nums[0 : st-1]中的某个,于是把memo清除掉
memo[st].clear();
}
}

0980differentPath3 不同路径3

1 题目

https://leetcode.cn/problems/unique-paths-iii/

2 解题思路

  • 1 解题思路:
    • 回溯,就暴力搜索就行,只需要考虑两个问题:
    • 什么时候进入下一个位置?没有出界,没有被访问过,不是障碍物
    • 什么时候返回?如果到edXY的时候,已经把所有0访问过,则认为找到一个方案,返回1,如果到edXY访问0的个数没达到总个数,则认为没找到,则返回0,最后将所有的返回值加起来就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Solution {
public:
int m,n;
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
int tarSum = 0;
int edX, edY;

int backTrack(vector<vector<int>>& grid, vector<vector<bool>>& vis, int x, int y, int curSum) {
if(x == edX && y == edY) {
// cout << "yes: " << curSum << endl;
if(curSum == tarSum) {
return 1;
} else {
return 0;
}
}

int ways = 0;
for(int mv = 0; mv < 4; ++mv) {
int nextX = x + dx[mv];
int nextY = y + dy[mv];
if(0 <= nextX && nextX < m && 0 <= nextY && nextY < n && \
!vis[nextX][nextY] && \
-1 != grid[nextX][nextY]
) {
vis[nextX][nextY] = true;
ways += backTrack(grid, vis, nextX, nextY, curSum + 1);
vis[nextX][nextY] = false;
}
}

return ways;
}


int uniquePathsIII(vector<vector<int>>& grid) {
m = grid.size();
n = grid[0].size();

// just backTracking all
vector<vector<bool>> vis(m, vector<bool>(n, false));

int x, y;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(1 == grid[i][j]) {
x = i; y = j;
} else if(2 == grid[i][j]) {
edX = i; edY = j;
++tarSum;
} else {
tarSum += (grid[i][j] == 0);
}
}
}
// cout << tarSum << endl;

int curSum = 0;
vis[x][y] = true;
return backTrack(grid, vis, x, y, curSum);
}
};

0996numSquarefulPermutations 正方形数组的排列个数

1 题目

https://leetcode.cn/problems/number-of-squareful-arrays/

2 解题思路

  • 1 解题思路:
    • 回溯获取全排列,backTrack带st参数,表示正在放第st个数字,注意此时nums[:st-1]是已经放好的排列,只需要在nums[st:]后半部分填充到st位置即可,也就是交换一下
    • 剪枝:若nums[st]和nums[st-1]的和不是完全平方数,则直接不进入下一个st
    • 记忆化搜索:考虑例子:2 2 2 2 2 2 2,使用memo[st][num],表示,在当前nums[:st-1]作为排列前缀的时候,num值放在st是否被搜索过,搜索过则标记一下,下一次不搜了
      • 需要注意一点:memo[st][num],表示在当前nums[:st-1]作为排列前缀在st放值时刻的num值的搜索情况,所以当st位置所有num值搜索完成,要返回进入下一个排列前缀搜索之前,记得clear掉,这个和0698这道题很像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Solution {
public:
void swap(int& a, int& b) {
int c = a;
a = b;
b = c;
}

bool judge(double tar) {
double x = sqrt(tar);
if(floor(x) == x) {
return true;
}
return false;
}

void print(vector<int>& vec) {
for(auto i : vec) {
cout << i <<" ";
}cout <<"\n";
}

unordered_set<string> rmDup;

// memo thah: cal[st][num] in pos st, whethter num has been visited
unordered_map<int, unordered_map<int, bool>> cal;

// st means : we are choosing a num for nums[st]
// and we should be sure that: nums[st] and nums[st-1] is able to sqrt
int permutationCheck(vector<int>& nums, int st) {
if(st == nums.size()) {
string tmp = "";
for(auto& i : nums) {
tmp += to_string(i);
}
if(rmDup.insert(tmp).second) {
return 1;
}
return 0;
}

int res = 0;
for(int i = st; i < nums.size(); ++i) {
swap(nums[i], nums[st]);
// cout << "level: " << st << " ";
// print(nums);
if(st == 0) {
if(!cal[st][nums[st]]) {
res += permutationCheck(nums, st + 1);
cal[st][nums[st]] = true;
}
} else {
if(judge(nums[st-1] + nums[st])) {
if(!cal[st][nums[st]]) {
res += permutationCheck(nums, st + 1);
cal[st][nums[st]] = true;
}
}
}
swap(nums[i], nums[st]);
}

// when return, the permutated prefix: nums[:st-1] will change, so we clear the memo
cal[st].clear();
return res;
}

int numSquarefulPerms(vector<int>& nums) {
int n = nums.size();
if(1 == n) {
return judge(nums.front());
}

// all sequence(permutation) and check each
return permutationCheck(nums, 0);
}
};

1240tilingRect 铺瓷砖

1 题目

https://leetcode.cn/problems/tiling-a-rectangle-with-the-fewest-squares/

2 解题思路

  • 1 解题思路:
    • 每一次从左上角,找出来目前能够铺的最大瓷砖,然后对于该位置,从最大瓷砖开始铺,铺好了进入下一层回溯,当下一层回溯完毕,对于当前位置,我们不再使用最大瓷砖,而使用长度小1的瓷砖,直到长度减小到1,对于每个起始位置,我们都从最大长度开始回溯,然后长度递减去回溯
    • 剪枝:当当前贴的瓷砖个数已经超过目前的值,我们直接return; 为什么要这个剪枝?比如一个10X10的瓷砖,一开始第一次就能铺满,那么等回溯到最后面都是1x1的瓷砖在贴,很慢很慢,那么就可以用这个已经得到的结果1,去提前返回,加速了回溯过程;就是如果无法获取更优结果就提前返回,在面试题17.25中也出现过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class Solution {
public:
vector<vector<bool>> board; // top left is 0, 0
int n, m; // n colums, m rows

int finRes = INT_MAX;

// start from left top
pair<int, pair<int, int>> findPos(vector<vector<bool>>& board) {
int x, y;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(!board[i][j]) {
x = i; y = j;
// find len:
int u = x;
while(u < m && !board[u][y]) {
++u;
}
int len = u - x;

int v = y;
while(v < n && !board[x][v]) {
++v;
}
len = min(len, v - y);

return {len, {x, y}};
}
}
}

return {-1, {-1, -1}};
}

void fillSquareWith(vector<vector<bool>>& board, int x, int y, int len, bool content) {
for(int i = x; i < x + len; ++i) {
for(int j = y; j < y + len; ++j) {
board[i][j] = content;
}
}
}

void backTrack(vector<vector<bool>>& board, int& leftCnt, int curRes) {
if(finRes <= curRes) {
return ;
}
if(0 == leftCnt) {
finRes = min(finRes, curRes);
return ;
}

// find first zero pos along the boarder, or the next 1
auto p = findPos(board);
int len = p.first;
int x = p.second.first, y = p.second.second;
// cout << "found max len: " << len << " x/y " << x << " " << y << endl;

for(int l = len; l > 0; --l) {
fillSquareWith(board, x, y, l, true);
leftCnt -= l*l;

// cout <<"ready to fill: x/y/l: " << x << "/" << y << "/" << l << "/" << "left: " << leftCnt << endl;
// print(board);
backTrack(board, leftCnt, curRes + 1);

leftCnt += l*l;
fillSquareWith(board, x, y, l, false);
}

}

int tilingRectangle(int n, int m) {
this->n = m;
this->m = n;
// cout << "this: " << this->n << " " << this->m << endl;
board.resize(n, vector<bool>(m, 0));
int leftCnt = n*m;
int curRes = 0;
backTrack(board, leftCnt, curRes);
return finRes;
}

void print(vector<vector<bool>>& board) {
for(auto v : board) {
for(auto i : v) {
cout << i << " ";
}cout << "\n";
}cout << "\n ---------- ";

}
}

1255maxScoreWords 最大单词分数从字母集合中

1 题目

https://leetcode.cn/problems/maximum-score-words-formed-by-letters/

2 解题思路

  • 1 解题思路:
    • 1.1 很显然:我们可以遍历所有的单词的集合,就是用或者不用,可以用状态压缩,也可以使用回溯
    • 1.2 这里我们使用回溯,主要就是:首先用,然后我们能得到一个分数,然后不用,我们也能得到一个分数,我们两者取最大值,记得在用了之后将其回溯,也就是把用掉的单词对应的字母再加回来
    • 1.3 返回结果:很显然,就是所有的单词都考虑到的时候返回0,就是对于编号st,每下一层回溯会让st++,那么等st和单词列表个数相等就返回就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Solution {
public:
vector<int> wordScore;

void printPool(unordered_map<char, int>& leftCharPool) {
for(auto& [c, cnt] : leftCharPool) {
cout << c << " " << cnt << endl;
}
cout << "---\n";
}

bool addOrRemove(string& word, unordered_map<char, int>& leftCharPool, bool add) {
// cout << "add/rm: " << word << endl;
if(add) {
for(auto& c : word) {
++leftCharPool[c];
}
} else {
unordered_map<char, int> curWord;
for(auto& c : word) {
++curWord[c];
}

for(auto& [c, cnt] : curWord) {
if(leftCharPool[c] < cnt) {
return false;
}
}

printPool(leftCharPool);
// remove the word from the pool
for(auto& [c, cnt] : curWord) {
// cout << "rm " << c << " : " << cnt << endl;
leftCharPool[c] -= cnt;
}
// printPool(leftCharPool);

}
return true;
}

int backTrack(vector<string>& words, int st, unordered_map<char, int>& leftCharPool) {
if(words.size() == st) {
return 0;
}

int curScore = INT_MIN;
// use it
if(addOrRemove(words[st], leftCharPool, false)) {
// cout << "using " << words[st] << endl;
// printPool(leftCharPool);
curScore = max(wordScore[st] + backTrack(words, st + 1, leftCharPool), curScore);
// add it back
addOrRemove(words[st], leftCharPool, true);
}
// not use it
// cout << "not using" << words[st] << endl;
// printPool(leftCharPool);
curScore = max(backTrack(words, st + 1, leftCharPool), curScore);
return curScore;
}

int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) {
// check all subsets of words using status compression
// you may use status compression, but here we use backTrack
unordered_map<char, int> leftCharPool;
wordScore.resize(words.size(), 0);
int wIdx = 0;
for(auto& w : words) {
for(auto& c : w) {
wordScore[wIdx] += score[c - 'a'];
}
++wIdx;
}

for(auto& c : letters) {
++leftCharPool[c];
}

int st = 0;
return backTrack(words, 0, leftCharPool);
}
};

0000_interview_1725wordMaxRectangle 单词矩阵

1 题目

https://leetcode.cn/problems/word-rectangle-lcci/

2 解题思路

  • 1 解题思路:
    • 1.1 很显然回溯即可:对于所有的同长度的单词,用回溯实现任意的permutation,hori记录放置的单词,vert记录垂直的单词,一单vert构成的前缀不存在,说明当前单词需要从新选择,回溯的深度我们无须考虑,因为vert不成功我们就提前返回了,然而vert成功的最基本的就是,回溯深度不可能超过最长的单词。
    • 1.2 我们从最长的单词出发,这样我们能够尽早的获取更大面积的单词矩阵,然后利用这个面积结果去跳过那些不可能比当前结果大的长度的单词集合,比如,已经发现最大面积为12,然后我们搜索长度为2的单词可能的矩阵,然后最大单词长度为6,我们就不用搜了,因为2为长度的单词集合,最大带来的面积就是2 * 6 = 12,我们就可以直接跳过了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class Solution {
public:
class Trie {
public:
vector<Trie*> curLevel;
bool isEnd = false;
Trie() : curLevel(26, nullptr) {
}

void buildTrie(vector<string>& words) {
for(auto& w : words) {
auto curNode = this;
for(auto& c : w) {
if(nullptr == curNode->curLevel[c - 'a']) {
curNode->curLevel[c - 'a'] = new Trie();
}
curNode = curNode->curLevel[c - 'a'];
}
curNode->isEnd = true;
}
}

bool preInTrie(string& w) {
auto curNode = this;
for(auto& c : w) {
auto nextLevel = curNode->curLevel[c - 'a'];
if(nullptr == nextLevel) {
return false;
}
curNode = nextLevel;
}
return true;
}

bool wordInTrie(string& w) {
auto curNode = this;
for(auto& c : w) {
auto nextLevel = curNode->curLevel[c - 'a'];
if(nullptr == nextLevel) {
return false;
}
curNode = nextLevel;
}
return curNode->isEnd;
}

};

void ableToMakeRes(vector<string>& hori, vector<string>& vert) {
// cout << "> hori.size() " << hori.size() << endl;
if(0 == vert[0].size()) {
// cout << "> vert empty: " << endl;
return;
}

for(auto& v : vert) {
if(!root->wordInTrie(v)) {
// cout << "> word failed: " << v;
return ;
}
}

int curSquare = vert[0].size() * vert.size();
// cout << "> curSquare: " << vert[0].size() * vert.size() << endl;
// cout << "> finSquare: " << finSquare << endl;
if(curSquare > finSquare) {
// cout << "updated!\n";
finSquare = curSquare;
resVec = hori;
}
}

// try all possible permutation of rect with width = w
void backTrack(
vector<string>& curWords,
vector<string>& hori,
vector<string>& vert,
vector<string>& resVec
) {
int width = curWords[0].size();
// cur found square bigger then all possilbe res from curWords
if(finSquare > width * maxWordLen) return;

// check if we obtain a res
ableToMakeRes(hori, vert);

for(auto& word : curWords) {
hori.push_back(word);
int vertCnt = 0;
bool failed = false;
for(int i = 0; i < width; ++i) {
vert[i].push_back(word[i]);
++vertCnt;
if(!root->preInTrie(vert[i])) {
failed = true;
break;
}
}
// cout << "trying: " << word << " | with hori = " << hori.size() << endl;

if(!failed) {
// cout << "choosing: " << word << endl;
// next level
backTrack(curWords, hori, vert, resVec);
}

// backTrack
// cout << "failed on: " << word << endl;
hori.pop_back();
for(int i = 0; i < vertCnt; ++i) {
vert[i].pop_back();
}
}

}
unique_ptr<Trie> root;
int finSquare = INT_MIN;
vector<string> resVec;

int maxWordLen = -1;

vector<string> maxRectangle(vector<string>& words) {
root = make_unique<Trie>();
root->buildTrie(words);

map<int, vector<string>, std::greater<int>> lenWords;
for(auto& w : words) {
lenWords[w.size()].push_back(w);
maxWordLen = max(maxWordLen, static_cast<int>(w.size()));
}

for(auto& [w, curWords] : lenWords) {
vector<string> hori;
vector<string> vert(w, "");
// cout << "w is: " << w <<endl;
backTrack(curWords, hori, vert, resVec);
}
return resVec;
}
};