杂题集合 (哈希 双指针 字符串 KMP DFS)
文章目录
454. 四数相加Ⅱ
从 A, B, C, D 中各挑一个数满足
a + b + c + d = 0
,求可能的组合个数。1 ≤ n ≤ 500
根据数据量判断只能用 O(n2logn) 及以下的时间复杂度,典型的空间换时间问题,用一个哈希表预处理 C + D
之和。
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> mp;
for (auto c : C)
for (auto d : D)
mp[c + d] ++;
int res = 0;
for (auto a : A)
for (auto b : B)
res += mp[-(a + b)];
return res;
}
15. 三数之和 18. 四数之和
一个数组中选择不同的
i, j, k
使nums[i] + nums[j] + nums[k] = 0
,求所有的组合方案。3 ≤ n ≤ 3000
多个数的问题,基本上是要定1个或者2个数,也就是枚举这些数,剩下的看有没有性质。
本题是典型双指针的运用,但是需要注意如何去除重复情况,如[1, 1, 1, -2]
只能算作1种。
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
sort(nums.begin(), nums.end());
for (int i = 0; i < n; i ++)
{
if (i && nums[i] == nums[i - 1]) continue;
int l = i + 1, r = n - 1;
while (l < r)
{
int sum = nums[i] + nums[l] + nums[r];
if (sum < 0) l ++;
else if (sum > 0) r --;
else
{
res.push_back({nums[i], nums[l], nums[r]});
l ++, r --;
while (l < r && nums[l] == nums[l - 1]) l ++;
while (l < r && nums[r] == nums[r + 1]) r --;
}
}
}
return res;
}
28. 找出字符串中第一个匹配项下标
字符串Hash
unsigned long long p[10010], h[10010];
int P = 131;
unsigned long long query(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int strStr(string s, string t) {
int n = s.length(), m = t.length(); s = " " + s, t = " " + t;
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + s[i];
}
unsigned long long target = 0;
for (int i = 1; i <= m; i ++ ) target = target * P + t[i];
for (int l = 1; l + m - 1 <= n; l ++ )
{
int r = l + m - 1;
if (query(l, r) == target) return l - 1;
}
return -1;
}
KMP AcWing 831. KMP字符串
找到所有原串
s
中和模板串t
相匹配的子串的起始下标。
KMP
习惯上下标从 1
开始。
next[i]
: 模板串[1 ~ i]
的子串,前后缀相同的最长子串的长度。
举个例子直观理解,模板串
t = "abababab"
。
next[1] = 0
,next[1]
为特例,直接设定成0
。
next[2] = 0, next[3] = 1, next[4] = 2, next[5] = 3, next[6] = 4, next[7] = 5, next[8] = 6
。我们以
next[5]
为例,子串为ababa
,最长的相同前后缀的就是aba
。
匹配过程中都是 p[j + 1]
和 s[i]
比较,如果相等那就比较两个串下一个字符,如果不相等,那 j
就跳到 next[j]
的位置。
原串
s = "abababc"
模板串t = "abababab"
当
i = 7
的时候,j + 1 = 7
,s[i] != s[j + 1]
此时应该j = next[j]
。那为什么能够这样跳转呢,因为
s[1 ~ 6]
和模板串的t[1 ~ 6]
相等,同时next[6] = 4
这意味着模板串以s[6]
结尾的子串ababab
其最长匹配的前后缀为abab
。因此,由于s[3 ~ 6] == t[3 ~ 6]
同时t[1 ~ 4] == t[3 ~ 6]
,故t[1 ~ 4] == s[3 ~ 6]
因此可以避免从头开始匹配模板串,直接j
指针从j + 1 = 5
的位置和i = 7
比较。
int n, m;
string s, p;
int ne[N];
int main()
{
cin >> m >> p >> n >> s;
p = " " + p, s = " " + s;
// 构造next数组过程
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++;
ne[i] = j;
}
// 匹配过程
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++;
if (j == m) // 成功匹配
{
cout << i - m + 1 - 1 << " "; // 要求原串从下标0开始
j = ne[j];
}
}
return 0;
}
459. 重复的子字符串
找字符串
s
是否存在一个循环节。
KMP
找循环节的经典问题,KMP
有以下2个结论需要记住:
(1) n - next[n]
的含义就是该字符串的最小周期,但是可能最后一段不完整;
(2) 如果周期可以整除 n
,那最小周期一定是其他周期的约数,也就是说如果最小周期不能整除 n
,那一定没有周期;
bool repeatedSubstringPattern(string s) {
int n = s.size(); s = " " + s;
vector<int> ne(n + 1);
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && s[i] != s[j + 1]) j = ne[j];
if (s[i] == s[j + 1]) j ++;
ne[i] = j;
}
int t = n - ne[n];
return t < n && n % t == 0;
}
151. 反转字符串中的单词
反转字符串中的单词顺序, " the sky is blue " → “blue is sky the”。
空间复杂度要求O(1)
-
去除多余空格,把单词移到字符串前面;2. 反转整个字符串;3.反转每个单词内部顺序。
其中第2步和第3步可以互换。
string reverseWords(string s) {
int k = 0;
for (int i = 0; i < s.length(); i ++ )
{
if (s[i] == ' ') continue;
int t = k, j = i;
while (j < s.length() && s[j] != ' ') s[t ++] = s[j ++];
reverse(s.begin() + k, s.begin() + t);
s[t ++] = ' ';
k = t, i = j;
}
if (k) k --; // 去掉最后一个单词后面的空格
s.erase(s.begin() + k, s.end());
reverse(s.begin(), s.end());
return s;
}
40. 组合总和 Ⅱ
找出
nums
中所有可以使数字和为target
的组合,每个数字只能使用一次 ,解集不能包含重复的组合。
nums = [10, 1, 2, 7, 6, 1, 5], target = 8
res = {{1, 1, 6}, {1, 2, 5}, {1, 7}, {2, 6}}
难点在于如何去除重复的组合,首先要明确会出现哪些重复组合(nums
先排好序):
本题是允许树枝重复{1, 1, 6}
,不允许树层重复。
void dfs(int sum, int start, vector<int> nums, int target) {
if (sum >= target) {
if (sum == target) res.push_back(ans);
return;
}
for (int i = start; i < nums.size(); i ++ ) {
if (i && nums[i] == nums[i - 1] && !st[i - 1]) continue;
ans.push_back(nums[i]);
st[i] = true;
dfs(sum + nums[i], i + 1, nums, target);
ans.pop_back();
st[i] = false;
}
}
vector<vector<int>> combinationSum2(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
dfs(0, 0, nums, target);
return res;
}
90. 子集Ⅱ
对应每个物品选不选的问题,可能有多个重复物品,要求输出不重复的子集组合方案。
也就是对于
[1, 2, 2]
选择[1, 2]
和[1, 2]
是等价的。
舍弃之前每个物品选或不选的思想,改成只选物品,但是每次只能从 start
之后开始选。
void dfs(int start, vector<int> nums) {
res.push_back(ans);
if (start == nums.size()) return;
for (int i = start; i < nums.size(); i ++ ) {
if (i && nums[i] == nums[i - 1] && !st[i - 1]) continue; // 去除树层重复
st[i] = true;
ans.push_back(nums[i]);
dfs(i + 1, nums);
st[i] = false;
ans.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
dfs(0, nums);
return res;
}
491. 非递减子序列
要求输出一个序列中所有的非递减子序列。
由于本题求的是子序列,因此不能对数组进行排序,因此和之前的去重逻辑不同。
在每一层开一个哈希表,记录被搜索过的元素即可。
void dfs(int start, int last, vector<int> nums) {
if (path.size() >= 2) res.push_back(path);
if (start == nums.size()) return;
unordered_set<int> s;
for (int i = start; i < nums.size(); i ++ ) {
if (nums[i] < last || s.count(nums[i])) continue;
s.insert(nums[i]);
path.push_back(nums[i]);
dfs(i + 1, nums[i], nums);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
dfs(0, -110, nums);
return res;
}
131. 切割回文子串
给定一个串,将其进行分割,使得每个子串都是回文子串。
bool isValid(string s) {
string tmp = s;
reverse(tmp.begin(), tmp.end());
return s == tmp;
}
void dfs(int last, string s) {
if (last == s.length()) {
res.push_back(ans);
return;
}
for (int len = 1; len + last - 1 < s.length(); len ++ ) {
string cur = s.substr(last, len);
if (isValid(cur)) {
ans.push_back(cur);
dfs(len + last, s);
ans.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
dfs(0, s);
return res;
}
93. 复原IP地址
给定一个字符串,返回其对应的所有可能IP地址。
bool isValid(string s) {
if (s[0] == '0') {
if (s.length() == 1) return true;
else return false;
}
return stoi(s) <= 255;
}
void dfs(int u, int last, string ans, string s) {
if (u == 4) {
res.push_back(ans);
return;
}
if (u == 3) {
int len = s.length() - last;
if (len > 3 || len == 0) return;
string cur = s.substr(last, len);
if (isValid(cur)) {
string tmp = ans + cur;
dfs(u + 1, last, tmp, s);
}
} else {
for (int len = 1; last + len - 1 < s.length() && len <= 3; len ++ ) {
string cur = s.substr(last, len);
if (isValid(cur)) {
string tmp = ans + cur + ".";
dfs(u + 1, last + len, tmp, s);
}
}
}
}
vector<string> restoreIpAddresses(string s) {
if (s.length() > 12) return {};
dfs(0, 0, "", s);
return res;
}
37. 解数独
bool col[9][9], row[9][9], cell[3][3][9];
bool dfs(int x, int y, vector<vector<char>> &board) {
if (y == 9) x ++, y = 0;
if (x == 9) return true;
if (board[x][y] != '.') return dfs(x, y + 1, board);
for (char c = '1'; c <= '9'; c ++ ) {
int i = c - '1';
if (row[x][i] || col[y][i] || cell[x / 3][y / 3][i]) continue;
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
board[x][y] = c;
if (dfs(x, y + 1, board)) return true;
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;
board[x][y] = '.';
}
return false;
}
void solveSudoku(vector<vector<char>>& board) {
for (int i = 0; i < board.size(); i ++ )
for (int j = 0; j < board[0].size(); j ++ )
if (board[i][j] != '.') {
int num = board[i][j] - '1';
row[i][num] = col[j][num] = cell[i / 3][j / 3][num] = true;
}
dfs(0, 0, board);
}