lcr148.栈
按放入顺序推栈,能弹出的就及时弹出,最后栈空则符合要求。
判断 takeOut 序列是否符合栈的操作逻辑,因为题目中“特殊的数据结构”其实就是栈(先进后出)。
思路如下:
1. 用一个栈来模拟图书放入的过程。
2. 按 putIn 的顺序依次将图书推入栈中。
3. 每次推入后,检查栈顶元素是否和 takeOut 当前待匹配的元素相同:
- 如果相同,就弹出栈顶元素,并移动 takeOut 的匹配指针。
- 重复此检查,直到栈顶元素和待匹配元素不同。
4. 当 putIn 的元素全部处理完后,若栈为空,说明 takeOut 是正确的拿取序列,返回 true ;否则返回 false 。
class Solution {
public:
bool validateBookSequences(vector<int>& putIn, vector<int>& takeOut)
{
stack<int> st;
int i=0;
for(int& p:putIn)
{
st.push(p);
while(!st.empty() && st.top()==takeOut[i])
{
st.pop();
i++;
}
}
return st.empty();
}
};
lc2316.dfs无向图联通块计算
class Solution {
public:
long long countPairs(int n, vector<vector<int>> &edges) {
vector<vector<int>> g(n);
for (auto &e: edges) {
int x = e[0], y = e[1];
g[x].push_back(y);
g[y].push_back(x); // 建图
}
vector<int> vis(n);
function<int(int)> dfs = [&](int x) -> int {
vis[x] = true; // 避免重复访问同一个点
int size = 1;
for (int y: g[x]) {
if (!vis[y]) {
size += dfs(y);
}
}
return size;
};
long long ans = 0;
for (int i = 0, total = 0; i < n; i++) {
if (!vis[i]) { // 未访问的点:说明找到了一个新的连通块
int size = dfs(i);
ans += (long) size * total;
total += size;
}
}
return ans;
}
};
lc1042. 不领接植花
邻接表涂色
给花园涂色:
1. 建邻接表
2. 涂色时,先看它相邻的用了什么颜色(记在 used 里)。
3. 找第一个没被用过的颜色,给当前花园涂上。
最后返回所有花园的颜色数组
class Solution {
public:
vector<int> gardenNoAdj(int n, vector<vector<int>>& paths) {
// 邻接表存连接关系
vector<vector<int>> g(n);
for (auto& p : paths) {
// 花园编号转成0开始(原是1开始)
int x = p[0] - 1, y = p[1] - 1;
g[x].push_back(y);
g[y].push_back(x);
}
// 存每个花园的颜色(1-4)
vector<int> color(n);
for (int i = 0; i < n; i++)
{
// 记录邻居用过的颜色
bool used[5] = {false};
for (int j : g[i]) {
used[color[j]] = true;
}
// 找第一个没被用过的颜色
int c = 1;
while (used[c]) {
c++;
}
color[i] = c;
}
return color;
}
};
lc3186.施咒
dp[i] 表示“考虑前i个不同数字时,能得到的最大总和”。
排好序后,对每个数字,要么跳过它,要么选它(同时只能搭配前面和它差≥3的数字),始终选总和最大的方案。
注意,就在于对于重复值的处理
1. 先整理数据:统计每个数字出现的次数(比如数字5出现3次,总和就是5×3),再把数字从小到大排序。
2. 动态规划记录最大和:用 dp[i] 表示“考虑前i个不同数字时,能得到的最大总和”。
3. 核心逻辑:
- 对每个数字 x (带次数 c ),先找最前面一个“和 x 的差小于3”的数字位置 j (意味着 j 之前的数字都能和 x 共存)。
- 决策:要么不选 x ,总和就是 f[i] (继承前i-1个的结果);要么选 x ,总和就是 f[j] + x×c ( j 之前的最大和加上 x 的总贡献),取两者更大的那个。
4. 最终结果: f[n] 就是考虑所有数字后能得到的最大总和。
class Solution {
public:
long long maximumTotalDamage(vector<int>& power) {
unordered_map<int, int> cnt;
for (int x : power) {
cnt[x]++;
}
vector<pair<int, int>> a(cnt.begin(), cnt.end()); //hash转化方法
sort(a.begin(),a.end());
int n = a.size();
vector<long long> f(n + 1);
for (int i = 0, j = 0; i < n; i++)
{
auto& [x, c] = a[i];
while (a[j].first < x - 2)
{
j++; // 利用了单调性
}
f[i + 1] = max(f[i], f[j] + (long long) x * c);
//选不选
}
return f[n];
}
};
lc2222. 状态机dp
简单dp:
dp[k][j] 记录“选 k 个字符、最后是 j ”的方案数,
class Solution {
typedef long long ll;
public:
long long numberOfWays(string s)
{
int n=s.size();
if(n<3) return 0;
vector<vector<ll>> dp(4,vector<ll>(2,0));
dp[0][1]=dp[0][0]=1;
for(int i=0;i<n;i++) //遍历子串,填表
{
//k要倒着加,要不然就多加了
for(int k=3;k>=1;k--)
{
if(s[i]=='0')
dp[k][0]+=dp[k-1][1];
else
dp[k][1]+=dp[k-1][0];
}
}
return dp[3][0]+dp[3][1];
}
};
k 倒着遍历是为了 确保状态转移时, dp[k-1][...] 用的是“上一轮”的旧值,避免当前轮更新的
lc442. 数组中重复的数据
模拟hash映射的原理 o(n)且不额外空间
“原地标记”
- 压榨完num的价值,数组自身的下标和符号 代替哈希表,num遍历查看完一个元素后,还原地通过符号标记修改。
- 利用了数也在1,n范围的特性
找出数组里恰好出现两次的数,返回这些数。
常规思路(哈希表,空间不够好)
优化思路(原地标记,空间 O(1))
利用数组下标和元素值的特殊关系:
- 数组长度是 n,元素值范围是 [1, n] ,下标范围是 [0, n-1] 。
- 可以把下标和元素值对应起来(比如值为 num 的数,对应下标 num-1 ),通过改元素正负,标记这个数是否出现过。
具体步骤(遍历+标记)
1. 遍历数组,对当前数 num :
- 先取绝对值(因为可能被改过符号),算对应下标 index = |num| - 1 ;
- 看 nums[index] 的符号:
- 若为正:说明 num 是第一次出现,把 nums[index] 改成负数(标记“已访问”);
- 若为负:说明 num 之前出现过,现在是第二次,把 num 加入结果(这就是重复的数)。
举个例子
比如数组是 [4,3,2,7,8,2,3,1] :
- 遍历到 4 ,对应下标 4-1=3 , nums[3] 是 7 (正)→ 改成 -7 ;
- 遍历到 3 ,对应下标 3-1=2 , nums[2] 是 2 (正)→ 改成 -2 ;
- ……
- 遍历到 2 ,对应下标 2-1=1 , nums[1] 是 -3 (负)→ 说明 2 重复,加入结果;
- 最终找出所有重复两次的数。
核心巧妙点
用数组自身的下标和符号代替哈希表,不用额外空间,完美解决“找重复两次的数”的问题,时间 O(n)、空间 O(1) 。
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
vector<int> duplicates;
int n = nums.size();
for (int i = 0; i < n; i++) {
int num = nums[i];
int index = abs(num) - 1;
if (nums[index] > 0) {
nums[index] = -nums[index];
}
else {
duplicates.push_back(index + 1);
}
}
return duplicates;
}
};