LeetCode
class Solution {
public:
// 题目要求的接口函数
int climbStairs(int n) {
return helper(n); // 调用自定义函数
}
private:
// 自定义辅助函数(带记忆化)
int helper(int n) {
if(n <= 2) return n;
return helper(n-1) + helper(n-2);
}
};
动态规划
最大子序和
1 | 线性 DP | 最大子序和 | LC-53 | 状态转移与决策分析 | ✅ | 2 |
---|
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int sz = nums.size();
int f[100005];
f[0] = nums[0];
int max_val = f[0];
for(int i = 1;i<sz;i++){
f[i] = max(nums[i],f[i-1]+nums[i]);
max_val = max(max_val,f[i]);
}
return max_val;
}
};
最小路径和
最小路径和 | LC-64 | 二维DP递推 | ✅ |
---|
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>>dp(m,vector<int>(n,0));
dp[0][0] = grid[0][0];
for(int i = 1;i<m;i++) dp[i][0] = dp[i-1][0] + grid[i][0];
for(int j = 1;j<n;j++) dp[0][j] = dp[0][j-1] + grid[0][j];
for(int i = 1;i<m;i++){
for(int j = 1;j<n;j++){
int x = dp[i-1][j];
int y = dp[i][j-1];
dp[i][j] = min(x,y)+grid[i][j];
}
}
return dp[m-1][n-1];
}
};
乘积最大子数组
乘积最大子数组 | LC-152 | 状态转移变种(维护最大最小值) | ✅ |
---|
class Solution {
public:
int maxProduct(vector<int>& nums) {
int sz = nums.size();
int max_product = nums[0];
int min_product = nums[0];
int result = nums[0];
for(int i = 1;i<sz;i++){
//用临时变量存储 这样才能保证临时最小值求的是按照前i-1个数求的
//因为这样max_product没在求临时最大值时改变
int temp_max_product = max({nums[i],min_product*nums[i],max_product*nums[i]});
int temp_min_product = min({nums[i],min_product*nums[i],max_product*nums[i]});
max_product = temp_max_product;
min_product = temp_min_product;
result = max(max_product,result);
}
return result;
}
};
由于 max_product
是当前的 max_product*nums[i]
/ min_product*nums[i]
/ nums[i]
中选的
所以即使 max_product
可能比 max_product*nums[i]
/ min_product*nums[i]
/ nums[i]
三者大
更新后的 max_product
也是 max_product*nums[i]
/ min_product*nums[i]
/ nums[i]
中的最大值
因此要有个全局变量 result来维护最大值
⭐编辑距离
编辑距离 | LC-72 | 经典双序列DP | ✅ |
---|
class Solution {
public:
int minDistance(string word1, string word2) {
int sz1 = word1.size();
int sz2 = word2.size();
vector<vector<int>> dp(sz1 + 1, vector<int>(sz2 + 1, 0));
//初始化
for(int i = 0;i<=sz1;i++) dp[i][0] = i;
for(int j = 0;j<=sz2;j++) dp[0][j] = j;
//状态转移 dp[i][j]代表word1的前i个字母要修改到和word2的前j个字母 所需的次数
//分三个操作
//插入 相当于再word1的第i+1位置插入和word2的第j个字母对应的字母 接下来要处理到word1的1~i和word2的1~(j-1)一样即可
//删除 dp[i-1][j] word1的第i个删除,要考虑word1的前i-1个变化到和word2的前j个相等
//替换 dp[i-1][j-1]
for(int i = 1;i<=sz1;i++){
for(int j = 1;j<=sz2;j++){
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]})+1;
}
}
}
return dp[sz1][sz2];
}
};
⭐ 导弹拦截
导弹拦截 | 洛谷-P1020 | 最长不升子序列+贪心优化+二分 (卡时间) | |||
---|---|---|---|---|---|
关于upper_bound和lower_bound |
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int nums[N];
int main() {
int n = 0;
while (cin >> nums[n]) n++; // 读取输入数据
// 第一问:最长不升子序列
vector<int> lis;
for (int i = 0; i < n; ++i) {
if (lis.empty() || nums[i] <= lis.back()) {
lis.push_back(nums[i]);
} else {
auto it = upper_bound(lis.begin(), lis.end(), nums[i], greater<int>());
*it = nums[i];
}
}
cout << lis.size() << endl;
// 第二问:最长上升子序列(Dilworth定理)
vector<int> seq;
for (int i = 0; i < n; ++i) {
if (seq.empty() || nums[i] > seq.back()) {
seq.push_back(nums[i]);
} else {
auto it = lower_bound(seq.begin(), seq.end(), nums[i]);
*it = nums[i];
}
}
cout << seq.size();
return 0;
}
求最长不上升子序列时
利用了贪心+二分
其中贪心策略是,
- 维护一个递减数组
lis
。 - 遍历每个元素
x
:- 若
x ≤ lis.back()
,直接加入lis
。 - 否则,找到
lis
中第一个比x
小的位置,用x
替换该位置的值。
- 若
- 最终
lis
的长度即为 LNIS 的长度。
因为当x ≤ lis.back()
时,LNIS 的长度不会有变化发生
只有x > lis.back()
时,可以将 x 添加到lis
的末尾,此时 LNIS 的长度才会增加
⭐⭐⭐
- 核心思想:让
lis
的每个位置保存该长度下最大的末尾元素
⭐⭐⭐
俄罗斯套娃信封问题
俄罗斯套娃信封问题 | LC-354 | 二维LIS优化 | ✅ |
---|
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
// 正确排序:宽度升序,相同宽度则高度降序
sort(envelopes.begin(), envelopes.end(), [](const vector<int>& a, const vector<int>& b) {
if (a[0] == b[0]) return a[1] > b[1];
return a[0] < b[0];
});
// 提取高度数组并寻找LIS
vector<int> lis;
for (const auto& env : envelopes) {
int h = env[1];//高度
auto it = lower_bound(lis.begin(), lis.end(), h);
if (it == lis.end()) {
lis.push_back(h);
} else {
*it = h;
}
}
return lis.size();
}
};
/*
将排序改为先按宽度升序,宽度相同按高度降序。
在构建LIS时,仅比较高度,因为宽度已经通过排序处理好了。
使用标准的LIS算法处理高度数组。
排序后的数组是按照宽度升序,高度降序,那么相同宽度的信封不会相互干扰,
因为它们的高度是降序的,这样后面遇到的高度只会更小,避免重复排序的问题
也就是说这样排序后
为 a3 a2 a1 b2 b1 c2 c1(a<b<c)
则单独看高度时为
[3 2 1] [2 1] [2 1]
找高度的LIS,每组之内至多只有一个数字在LIS中
不会有多个,因为本身每一组中高度是降序的,不会在升序序列中出现
*/
通配符匹配
通配符匹配 | LC-44 | 复杂模式匹配DP | ✅ | ⭐ |
---|
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size();
int n = p.size();
vector<vector<int >> dp(m + 1, vector<int>(n + 1));
dp[0][0] = true;//空匹配空
for (int i = 1; i <= m; i++) dp[i][0] = false;
for (int i = 1; i <= n; i++) {
if (p[i - 1] == '*') {
dp[0][i] = true;
} else {
break;
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s[i - 1] == p[j - 1] || p[j - 1] == '?') dp[i][j] = dp[i - 1][j - 1];
else if (p[j - 1] == '*') {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
//略过'*'去匹配P的下一位||保留'*'的活性 匹配s的下一位
}
}
}
return dp[m][n];
}
};
重点在于理解 dp[i][j] = dp[i-1][j]||dp[i][j-1]
这句的意思是,当 p[j-1]
为 ‘*’
的时候,因为 ‘*’
可以匹配 0 个或者多个字符
- 所以当
‘*’
视为空时(匹配 0 个),相当于直接掠过 p 的这一位,从原先的由s[i-1]
与p[j-1]
是否匹配的状态转移成了现在的看s[i-1]
和p[j]
是否匹配的转移 - 当
‘*’
视为匹配多个s
的字符时,相当于p[j-1]
的这个‘*’
一直保持活性,固定p
的视角在p[j-1] == ‘*’
这里,移动's'
进行比较
背包问题
数字组合 (0-1背包)
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[105];
int dp[105][10005];
/*
dp[i][j]前i个数中选 恰好等于j的方案数
dp[i][j] = max(dp[i-1][j], dp[i-1][j-a[i]]+1)
前i-1个数中恰好等于j-a[i]的方案数
*/
int main(){
cin>>n>>m;
for(int i = 1;i<=n;i++){
cin>>a[i];
}
dp[0][0] = 1;
//前n个数 dp初始化
for(int i = 1;i<=n;i++){
for(int j = 0;j<=m;j++){
dp[i][j] = dp[i-1][j];
if(j>=a[i]){
dp[i][j] += dp[i-1][j-a[i]];
}
}
}
cout<<dp[n][m];
return 0;
}
定义 dp[i][j]
表示 前 i
个数中选出若干个数,和为 j
的方案数。
- 维度解释:
i
:前i
个数(决策阶段)。j
:当前目标和(状态变量)。
1. 决策分析
对于第 i
个数 a[i]
,有两种选择:
- 不选:方案数等于前
i-1
个数中和为j
的方案数,即dp[i-1][j]
。 - 选(仅当
j >= a[i]
):方案数等于前i-1
个数中和为j - a[i]
的方案数,即dp[i-1][j - a[i]]
。
2. 方程形式化
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
−
1
]
[
j
−
a
[
i
]
]
if
j
≥
a
[
i
]
d
p
[
i
−
1
]
[
j
]
otherwise
dp[i][j] = \begin{cases} dp[i-1][j] + dp[i-1][j - a[i]] & \text{if } j \geq a[i] \\dp[i-1][j] & \text{otherwise}\end{cases}
dp[i][j]={dp[i−1][j]+dp[i−1][j−a[i]]dp[i−1][j]if j≥a[i]otherwise
搜索
DFS && BFS
岛屿数量 (连通块问题)
相当于是图的 DFS 和 BFS
1-2 | 搜索 | 岛屿数量 | LC-200 | 中等 | DFS/BFS基础(连通块问题)方向数组 | ✅ | Yes, 经典的模板题 |
---|
//DFS做法
class Solution {
private:
void dfs(vector<vector<char>>& grid, int x,int y){
int m = grid.size();
int n = grid[0].size();
grid[x][y] = 0;//代表访问过;
if(x-1>=0&&grid[x-1][y] == '1') dfs(grid,x-1,y);
if(x+1<m&&grid[x+1][y] == '1') dfs(grid,x+1,y);
if(y-1>=0&&grid[x][y-1] == '1') dfs(grid,x,y-1);
if(y+1<n&&grid[x][y+1] == '1') dfs(grid,x,y+1);
}
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
int res = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
{
res++;
dfs(grid,i,j);
}
}
}
return res;
}
};
//BFS做法
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
if(!m) return 0;
int n = grid[0].size();
//存放坐标
queue <pair<int,int>> adj;
int res = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(grid[i][j] == '1'){
res ++ ;
adj.push({i,j});
while(!adj.empty()){
//获取队列中首元素的坐标后输出
auto rc = adj.front();
adj.pop();
int r = rc.first;
int c = rc.second;
grid[r][c] == '0';
if(r-1>=0 && grid[r-1][c] == '1'){
adj.push({r-1,c});
grid[r-1][c] = '0';
}
if(c-1>=0 && grid[r][c-1] == '1'){
adj.push({r,c-1});
grid[r][c-1] = '0';
}
if(r+1<m && grid[r+1][c] == '1'){
adj.push({r+1,c});
grid[r+1][c] = 0;
}
if(c+1<n && grid[r][c+1] == '1'){
adj.push({r,c+1});
grid[r][c+1] = '0';
}
}
}
}
}
return res;
}
};
二叉树的层序遍历(树的BFS遍历)
相当于是树的 BFS
二叉树的层序遍历 | LC-102 | 中等 | BFS模板(队列应用) | ✅ |
---|
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(!root) return res;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
int levelSize = q.size();
vector<int> levelVal;
for(int i = 0; i < levelSize; ++i) {
TreeNode* tmp = q.front();
q.pop();
levelVal.push_back(tmp->val);
// 修正点:判断子节点存在才入队
if(tmp->left) q.push(tmp->left);
if(tmp->right) q.push(tmp->right);
}
res.push_back(levelVal);
}
return res;
}
};
初始化队列,如果根节点存在则入队。
然后,当队列不为空时:
- 获取当前层的节点数量size = q.size()
- 创建一个临时vector level
- 循环size次:
- 取出队列前端节点
- 将节点值加入level
- 如果有左子节点,入队
- 如果有右子节点,入队
- 将level添加到res中
这样就能正确分层处理节点,并且避免越界问题。
离开中山路(BFS求图中最短路径)
离开中山路 | 洛谷 P1706 | 简单 | BFS最短路径基础 | ✅ |
---|
#include <bits/stdc++.h>
using namespace std;
int n, st_x, st_y, ed_x, ed_y;
char grid[1005][1005];
int dist[1005][1005]; // 距离数组兼做访问标记
int main() {
cin >> n;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
cin >> grid[i][j];
cin >> st_x >> st_y >> ed_x >> ed_y;
// 检查起点合法性
if (grid[st_x][st_y] != '0') {
cout << -1;
return 0;
}
queue<pair<int, int >> q;
memset(dist, -1, sizeof(dist));//距离一开始都设置为不可达
q.push({st_x, st_y});//起始点
dist[st_x][st_y] = 0;//起点到起点的距离为0
// 四方向数组
int dirs[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
while(!q.empty()) {
auto[x, y] = q.front();
q.pop();
if(x == ed_x && y == ed_y) {
cout << dist[x][y];
return 0;
}
for(auto [dx, dy] : dirs) {
int nx = x+dx;
int ny = y+dy;
if(nx>=1&&nx<=n&&ny>=1&&ny<=n&&//在范围内
grid[nx][ny] == '0'&&//是路
dist[nx][ny]==-1//未访问过
){
dist[nx][ny] = dist[x][y]+1;
q.push({nx,ny});
}
}
}
cout << -1; //不可达
return 0;
}
记忆化搜索:斐波那契数
记忆化搜索示例:斐波那契数 | LC-509 | 简单 | 记忆化搜索入门 |
---|
class Solution {
private:
int dfs(int n ,vector<int>& memo){
if(memo[n] != -1) return memo[n];
if(n<=1){
memo[n] = n;
return n;
}
memo[n] = dfs(n-1,memo)+dfs(n-2,memo);
return memo[n];
}
public:
int fib(int n) {
vector<int>memo(n+2,-1);
return dfs(n,memo);
}
};
迷宫问题 (求路径条数回溯算法)
迷宫问题 | 洛谷 P1605 | 普及- | DFS回溯(路径搜索) |
---|
#include<bits/stdc++.h>
using namespace std;
int n,m,t,st_x,st_y,ed_x,ed_y;
int grid[6][6];//迷宫
int res;
//0是路 1是障碍
void dfs(int x,int y){
if(x == ed_x&&y==ed_y){
res++;
return ;
}//到达终点
if(grid[x][y]!=0){
return ;//撞 障碍物/走过的部分 了
}
if(x<1||y<1||x>n||y>m){
return ;//撞墙
}
grid[x][y] = 1;//标记走过了
dfs(x+1,y);
dfs(x-1,y);
dfs(x,y+1);
dfs(x,y-1);
grid[x][y] = 0;
}
int main(){
//长n宽m,t个障碍
cin>>n>>m>>t;
//起点 终点坐标
cin>>st_x>>st_y>>ed_x>>ed_y;
for(int i = 0;i<t;i++){
int x,y;
cin>>x>>y;
grid[x][y] = 2;//代表障碍
}
//DFS
if(grid[ed_x][ed_y]!=0){
cout<<0;
return 0;
}
dfs(st_x,st_y);
cout<<res;
return 0;
return 0;
}
单词搜索 (DFS判断有无正确路线)
单词搜索 | LC-79 | 中等 | DFS回溯(二维矩阵搜索) |
---|
class Solution {
private:
bool found = false;
void dfs(int x,int y,int& res,vector<vector<char>>& board,string word,vector<vector<int>>&vis){
if(found) return ;//找到了 return
int m = board.size();
int n = board[0].size();
if(res == word.size()-1){
found = true;
return ;
}
vis[x][y] = 1;
res ++;
if(x-1>=0&&board[x-1][y] == word[res]&&vis[x-1][y]!=1){
dfs(x-1,y,res,board,word,vis);
}
if(y-1>=0&&board[x][y-1] == word[res]&&vis[x][y-1]!=1){
dfs(x,y-1,res,board,word,vis);
}
if(x+1<m&&board[x+1][y] == word[res]&&vis[x+1][y]!=1){
dfs(x+1,y,res,board,word,vis);
}
if(y+1<n&&board[x][y+1] == word[res]&&vis[x][y+1]!=1){
dfs(x,y+1,res,board,word,vis);
}
res -- ;
vis[x][y] = 0;
}
public:
bool exist(vector<vector<char>>& board, string word) {
int m = board.size();
int n = board[0].size();
//选出和word第一个字符一样的点的位置
//存放在rc中
vector<pair<int,int>>rc;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(board[i][j] == word[0]){
rc.push_back({i,j});
}
}
}
//遍历这几个和word[0]一样的点,以他们为起点 进行回溯
for(auto p : rc){
int res = 0;
int x = p.first;
int y = p.second;
vector<vector<int>>vis(m,vector<int>(n,0));
dfs(x,y,res,board,word,vis);
if(found){
return true;
}
}
return false;
}
};
在用 dfs 时,因为要用到递归,所以莫忘记写递归结束的条件
该题是 res == word.size() - 1
不要把条件写在 dfs 函数外面(比如写在主函数中)
因为这样的话,dfs 就会一直循环,进行多次重复计算
并且该题目是只要有一种结果就是 true,所以可以用 found 变量,这样可以防止某一次已经找到了最终结果但仍重复计算的事情发生
滑雪(DFS记忆化搜索寻找最长路径)
滑雪 | 洛谷 P1434 | 普及+/提高 | 记忆化搜索(DFS+DP) | ✅ |
---|
#include <bits/stdc++.h>
using namespace std;
int m , n;
int grid[105][105];
int dp[105][105];
int dx[4] = {0,0,1,-1};
int dy[4] = {1,-1,0,0};
int dfs(int x,int y){
if(dp[x][y] != 0) return dp[x][y];
dp[x][y] = 1;//每次到达这个点都要初始化
for(int i = 0;i<4;i++){
int xx = x+dx[i];
int yy = y+dy[i];
if(xx>=1&&xx<=m&&yy>=1&&yy<=n&&grid[xx][yy]<grid[x][y]){
dfs(xx,yy);
dp[x][y] = max(dp[x][y],dp[xx][yy]+1);
}
}
return dp[x][y];
}
int main(){
//m行n列
cin>>m>>n;
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
cin>>grid[i][j];
}
}
//
int ans = 0;
for(int i = 1;i<=m;i++){
for(int j =1;j<=n;j++){
ans = max(ans,dfs(i,j));
}
}
cout<<ans;
return 0;
}
dp[x][y]
表示 从坐标 (x, y)
出发,能滑行的最长路径的长度。
状态转移方程:
d
p
[
x
]
[
y
]
=
max
(
x
x
,
y
y
)
∈
neighbors
grid
[
x
x
]
[
y
y
]
<
grid
[
x
]
[
y
]
(
d
p
[
x
x
]
[
y
y
]
+
1
)
dp[x][y] = \max_{\substack{(xx, yy) \in \text{neighbors} \\ \text{grid}[xx][yy] < \text{grid}[x][y]}} \left ( dp[xx][yy] + 1 \right)
dp[x][y]=max(xx,yy)∈neighborsgrid[xx][yy]<grid[x][y](dp[xx][yy]+1)
⭐滑动谜题(BFS+状态压缩)
滑动谜题 | LC-773 | 困难 | BFS+回溯+记忆化搜索+DP | ✅ | 用了 BFS +状态压缩 | 2 |
---|
class Solution {
public:
int slidingPuzzle(vector<vector<int>>& board) {
string target= "123450";
string start;
//board拼接为start字符串
for(int i = 0;i<2;i++){
for(int j = 0;j<3;j++){
start +=(board[i][j] + '0');
}
}
//BFS初始化
queue<pair<string,int>>q;
vector<vector<int>>dirs = {{0,1},{0,-1},{1,0},{-1,0}};
unordered_set<string>vis;//若某个状态存在 则vis.count(state) != 0
//状态转移逻辑
q.push({start,0});//start 0步
vis.insert(start);//start 已访问
while(!q.empty()){
//取出来第一个元素
auto [state,steps] = q.front();
q.pop();//出队 勿忘
//破壳
if(state == target) return steps;
//找0的位置
int pos = state.find('0');
//0所在位置的坐标
int x = pos/3;
int y = pos%3;
for(int i = 0;i<4;i++){
int nx = x+dirs[i][0];
int ny = y+dirs[i][1];
if(nx>=0&&ny>=0&&nx<2&&ny<3){
string newState = state;
int newPos = nx*3+ny;
swap(newState[newPos],newState[pos]);
//如果没被访问过
if(!vis.count(newState)){
vis.insert(newState);
q.push({newState,steps+1});
}
}
}
}
return -1;
}
};
1. 状态压缩
将二维数组转换为字符串,例如:
初始状态 [[1,2,3],[4,0,5]] → 字符串 "123405"
目标状态 [[1,2,3],[4,5,0]] → 字符串 "123450"
2. BFS初始化
- 队列:存储待处理的状态和当前步数。
- 哈希集合:记录已访问状态,避免重复。
- 方向数组:定义
0
的移动方向(上下左右)。
3. 状态转移逻辑
- 找到
0
的位置:通过字符串的find
方法。 - 转换为二维坐标:计算行
x = pos / 3
,列y = pos % 3
。 - 尝试四个方向移动:
- 计算新坐标
(nx, ny)
。 - 检查是否越界(行范围
0~1
,列范围0~2
)。
- 计算新坐标
- 生成新状态:交换
0
和新位置的字符。
4. 终止条件
当前状态等于目标状态时,返回步数。
并查集
765. 情侣牵手
我们不关心具体交换过程,只关心“最终配对结果”。
把每对情侣看作一个编号(情侣对编号 = person_id / 2
),那么:
-
每两个相邻的人(
row[i]
,row[i+1]
)分别属于两个情侣编号。 -
如果这两个编号不是同一个,就表示“当前配错了位置”,我们就将这两个情侣编号合并成一个集合。
最终我们会得到若干个连通块(集合),每个块内的人只能通过内部交换来变正确。 -
每个集合内部,需要的最少交换次数是:
集合大小 - 1
-
所以所有集合最少交换次数总和是:
总对数 - 集合数量
class Solution {
private:
int fa[32];
int sets;
int find(int x){
if(fa[x] == x) return x;
int root = find(fa[x]);//递归找最深层次的根节点,找每次递归点的父节点的父节点的…
fa[x] = root;//扁平化处理
return root;
}
void unite(int x,int y){
if(find(x) != find(y)){
int root_x = find(x);
int root_y = find(y);
fa[root_x] = root_y;//x挂在y上
sets -- ;
}
}
public:
int minSwapsCouples(vector<int>& row) {
int sz = row.size();
int m = sz/2;
sets = sz/2;//集合个数
for(int i = 0;i<m;i++) fa[i] = i;//该题是对情侣编号进行操作的
for(int i = 0;i<sz;i+=2){
unite(row[i]/2,row[i+1]/2);//合并的是两者的情侣编号
}
return m - sets;
}
};
839. 相似字符串组
我们定义“相似”具有传递性:
如果 A ~ B 且 B ~ C,则 A ~ C
- 每个字符串视为图中的一个点。
- 如果两个字符串“相似”,就在它们之间连一条边。
- 那么整个问题就变成了:图中有多少个连通块(也就是「相似字符串组」)。
class Solution {
private:
int fa[302];
int sets ;
int find(int x){
if(x == fa[x]) return x;
int root = find(fa[x]);
fa[x] = root;//扁平化
return root;
}
void unite(int x,int y){
int root_x = find(x);
int root_y = find(y);
if(root_x != root_y){
fa[root_x] = root_y;
sets -- ;
}
}
bool isSimilarStr(string s1,string s2){
int m = s1.size();
int diff = 0;
for(int i = 0;i<m && diff<3;i++){
if(s1[i] != s2[i]){
diff ++;
}
}
if(diff == 0 || diff == 2) return true;
else return false;
}
public:
int numSimilarGroups(vector<string>& strs) {
int n = strs.size();//n个单词
sets = n;//sets个集合
for(int i = 0;i<n;i++) fa[i] = i;//每个单词指向自己
for(int i = 0;i<n;i++){
for(int j = i+1;j<n;j++){
if(isSimilarStr(strs[i],strs[j])){
unite(i,j);
}
}
}
return sets;
}
};
947. 移除最多的同行或同列石头
如果两个石头在同一行或同一列,那么我们可以通过某种顺序把它们全部“连通”起来(因为移除一个不会影响另一个的“可达性”)。
所以我们可以将这些石头抽象为图上的点,只要两个石头满足“同行或同列”,就认为它们之间有一条边,构成一个 连通块。
一个连通块里,总是可以移除
块中石头数 - 1
个石头(留一个不动就行)。
class Solution {
private:
int fa[1005];
int sets;
int find(int x){
if(x == fa[x]) return x;
int root = find(fa[x]);
fa[x] = root;//扁平化处理
return root;
}
void unite(int x,int y){
int root_x = find(x);
int root_y = find(y);
if(find(x) != find(y)){
fa[root_x] = root_y;
sets -- ;
}
}
public:
int removeStones(vector<vector<int>>& stones) {
map<int, int> firstRow; // 列,石头序号
map<int, int> firstCol; // 行,石头序号
int n = stones.size();
iota(fa,fa+n+1,0);
sets = n;
// 数据前置处理
for (int i = 0; i < n; i++) {
int col = stones[i][0];
int row = stones[i][1];
if (firstCol.find(col) == firstCol.end()) {
firstCol.insert({col, i});
} else {
unite(i, firstCol[col]); // 合并石头
}
if (firstRow.find(row) == firstRow.end()) {
firstRow.insert({row, i});
} else {
unite(i, firstRow[row]); // 合并石头
}
}
return n - sets;
}
};
2092. 找出知晓秘密的所有专家
这题的做法是:
首先专家 0 和 firstPerson 知道秘密,将他们俩合并
然后后续的会议按照时间戳排序
对于相同的时间内的会议做处理(筛选相同时间的操作看代码,很妙)
在这个时间内,一个个的按照并查集的方式分组,并且更新是否知晓秘密的信息
每一段相同的时间完成后,再重新遍历一遍会议,把无意义的会议造成的集合的合并撤销掉
最后按照 secret[find (x)]来得到答案
class Solution {
private:
int fa[100005];
bool secret[100005];
int find(int x){
if(x == fa[x]) return x;
int root = find(fa[x]);
fa[x] = root;//扁平化
return root;
}
void unite(int x,int y){
int fx = find(x);
int fy = find(y);
if(fx != fy){
fa[fx] = fy;//x挂在y上
secret[fy] |= secret[fx];//更新fy的信息
}
}
public:
vector<int> findAllPeople(int n, vector<vector<int>>& meetings, int firstPerson) {
iota(fa,fa+n,0);//每个人的父节点
for(int i = 0;i<n;i++) secret[i] = false;
secret[0] = true;
secret[firstPerson] = true;//知道秘密
//时间排序
sort(meetings.begin(),meetings.end(),[](const vector<int>& a,const vector<int>& b){
return a[2] < b[2];
});
//按照时刻做
int m = meetings.size();
for(int l = 0;l<m ;){
int r = l;
while(r+1<m && meetings[r+1][2] == meetings[l][2]){
r ++;//找到右边界
}
for(int i = l;i<=r;i++){
unite(meetings[i][0],meetings[i][1]);//合并此时的会议专家
}
//撤销选择
for(int i = l;i<=r;i++){
int a = meetings[i][0];
int b = meetings[i][1];
if(!secret[find(a)]){
fa[a] = a;
}
if(!secret[find(b)]){
fa[b] = b;
}
}
l = r + 1;
}
//计算答案
vector<int>ans;
for(int i = 0;i<n;i++){
if(secret[find(i)]){
ans.push_back(i);
}
}
return ans;
}
};
2421. 好路径的数目
该题目讲解
这题的做法是,除了开 fa 数组存放父节点外,还开一个 maxcnt 标记数组
然后按照边的两端的节点中的值更大的那个值升序(从小到大)排序
先处理边最小(也就是所有边中,边的顶点值的最大值最小的)的,后续依次遍历
把每次遍历到的边的两个点的所在的两个集合进行合并 (在同一个集合无需合并)
对于合并:要选择集合的代表顶点值更大的那个点做两个集合所合并成的集团的代表节点,然后若合并前的两个集合的代表节点的最大值相同,且数量为 m 和 n (由 cntmax[fa[x]]和 cntmax[fa[y]]
得到) ,则说明这两个集合可以构成的好路径的个数为 (m×n),因为 m 和 n 都是集合内节点的最大节点值,到另一个集合的最大节点的路径上,肯定没有比首尾节点(也就是这里的最大节点)还大的值了,所以每两个节点形成一条好路径。两两组合就是 m×n
最终的答案再加上每个节点到自身的路径(也是好路径)
class Solution {
private:
//const int MAXN = 30010;
int fa[30010];//father数组
int maxcnt[30010];//集团中的最大值出现次数
void build(int n){//节点个数
for(int i = 0;i<n;i++){
fa[i] = i;//指向自身
maxcnt[i] = 1;//初始都为1
}
}
int find(int x){
if(x == fa[x]) return x;
int root = find(fa[x]);//递归找root
fa[x] = root;//扁平化
return root;
}
int unite(int x,int y,vector<int>& vals){//返回好路径的条数
int path = 0;
int fx = find(x);
int fy = find(y);//既是集团的代表节点 也是集团中出现的最大的节点值的下标
if(vals[fx] > vals[fy]){//值小的合并在值大的集团上
fa[fy] = fx;
}else if(vals[fx] < vals[fy]){
fa[fx] = fy;
}else{//怎么合并无所谓 但是maxcnt要根据合并方法计数
path = maxcnt[fx]*maxcnt[fy];
fa[fx] = fy;//x合并在y上
maxcnt[fy] += maxcnt[fx];
}
return path;
}
public:
int numberOfGoodPaths(vector<int>& vals, vector<vector<int>>& edges) {
int n = vals.size();
build(n);
sort(edges.begin(),edges.end(),[&](const vector<int>& a,const vector<int>&b){//按照边的最大节点的大小排序
int val_a = max(vals[a[0]],vals[a[1]]);
int val_b = max(vals[b[0]],vals[b[1]]);
return val_a < val_b;//从小到大排序
});
//并查集处理
int ans = n;
for(auto edge : edges){
ans += unite(edge[0],edge[1],vals);
}
return ans;
}
};
小题
罗马数字转整数 (哈希表映射)
★小题 | 罗马数字转整数 | LC-13 | 哈希表映射 | ✅ |
---|
class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> roman = {
{'I',1}, {'V',5}, {'X',10}, {'L',50},
{'C',100}, {'D',500}, {'M',1000}
};
int total = 0;
for(int i=0; i<s.size(); ++i){
// 当前字符值 < 后一个字符值 → 减去当前值
if(i+1 < s.size() && roman[s[i]] < roman[s[i+1]]){
total -= roman[s[i]];
} else {
total += roman[s[i]];
}
}
return total;
}
};
婚配问题 (GS 算法)
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
#include <map>
#include <string>
using namespace std;
int main() {
int x;
cin >> x;
bool first = true;
while (x--) {
if (!first) cout << endl;
first = false;
int n;
cin >> n;
vector<char> male_names(n), female_names(n);
for (int i = 0; i < n; ++i) cin >> male_names[i];//输入男性名字
for (int i = 0; i < n; ++i) cin >> female_names[i];//输入女性名字
//男性名字 女性名字 映射序号
map<char, int>male_map, female_map;
for (int i = 0; i < n; i++) {
male_map[male_names[i]] = i;//'a' -> 0
female_map[female_names[i]] = i;//'A' -> 0
}
//=====男性偏好索引列表=====
vector<vector<int> > male_prefs(n); //存储偏好索引
for(int i = 0; i<n; i++) {
string line;
cin >> line;
//此行对应的男性名称
char c = line[0];
//喜欢的那几个提取出来
string line_male_prefs = line.substr(2);
int male_index = male_map[c];//当前行的男性序号
for (int j = 0; j < line_male_prefs.size(); ++j) { // 修改为传统循环
char x = line_male_prefs[j];
int female_index = female_map[x];//当前男性喜欢的每个女性的序号
male_prefs[male_index].push_back(female_index);
}
}
//=====女性偏好索引列表====
vector<vector<int> > female_prefs(n); //同上
vector<vector<int> > female_rank(n, vector<int>(n)); //存储女性对男性的排名
for (int i = 0; i < n; i++) {
string line ;
cin >> line;
//此行对应的女性名字
char c = line[0];
//提取喜欢的人的序号
string line_female_prefs = line.substr(2);
int female_index = female_map[c];//当前行的女性序号
for (int j = 0; j < line_female_prefs.size(); ++j) { // 修改为传统循环
char x = line_female_prefs[j];
int male_index = male_map[x];//当前行女性喜欢的男性的序号的映射
female_prefs[female_index].push_back(male_index);//喜欢的每个女性push进去
}
//反向数组 存储女性对男性的喜欢程度排名,用于下面的配偶调换
for (int j = 0; j < n; j++) {
int m = female_prefs[female_index][j];
female_rank[female_index][m] = j;
}
}
//=====输入的数据处理完毕=====
/*
a:BAC
b:BAC
c:ACB
A:acb
B:bac
C:cab
则
male_prefs为 [[1 0 2],[1 0 2],[0 2 1]]
female_prefs为 [[0 2 1],[1 0 2],[2 0 1]]
frmale_prefs[0][2] = 1
0号女第(2+1)喜欢的男性是1号男
==>
female_rank[0][1] = 2;
0号女对1号男的喜欢顺序是第(2+1)
*/
//=====开始GS算法========
vector<int> wife(n, -1); // wife[m] = 男性m当前匹配的女性索引
vector<int> husband(n, -1); // husband[f] = 女性f当前匹配的男性索引
stack<int> q; // 存储当前单身的男性
for (int i = 0; i < n; i++) q.push(i); // 初始所有男性入栈
while (!q.empty()) {
int cur_index = q.top();//当前未婚配男性
q.pop();
//如果当前男性没有配偶,对于喜欢列表的女性依次匹配
for (int k = 0; k < male_prefs[cur_index].size(); ++k) { // 修改为传统循环
int p = male_prefs[cur_index][k];
if (wife[cur_index] != -1) break; // 已匹配则跳过
if (husband[p] == -1) {
husband[p] = cur_index;//男性找到配偶
wife[cur_index] = p;//女性找到配偶
break;//退出循环
} else if (husband[p] != -1) { //当前女性有配偶
int this_woman_husband = husband[p];
//p对cur_index对应的男的喜欢程度比原配更大,则换
//这是排名 越小越喜欢
if (female_rank[p][cur_index] < female_rank[p][this_woman_husband]) {
//当前匹配成功
husband[p] = cur_index;
wife[cur_index] = p;
//原配偶变为单身
wife[this_woman_husband] = -1;
//入单身栈
q.push(this_woman_husband);
break;
} else {
//p对cur_index喜欢程度更小
continue;
}
}
}
}
//========== 按字典序输出结果 ==========
vector<char> sorted_males = male_names;
sort(sorted_males.begin(), sorted_males.end()); // 按字母顺序排序
for (int i = 0; i < sorted_males.size(); ++i) { // 修改为传统循环
char c = sorted_males[i];
int m = male_map[c]; // 获取男性索引
int f = wife[m]; // 获取匹配的女性索引
cout << c << " " << female_names[f] << endl;
}
}
return 0;
}