lc3310
dfs标记可疑方法
入度+连通块

class Solution {
public:
vector<int> remainingMethods(int n, int k, vector<vector<int>>& invocations)
{
vector<vector<int>> g(n);
for (auto& e : invocations)
++g[e[0]].push_back(e[1]);++
// 标记所有可疑方法
vector<int> is_suspicious(n);
auto dfs = [&](auto&& dfs, int x) -> void {
is_suspicious[x] = true;
++for (int y : g[x]) {++
if (!is_suspicious[y]) { // 避免无限递归
dfs(dfs, y);
}
}
};
dfs(dfs, k);
++// 检查是否有【非可疑方法】->【可疑方法】的边
for (auto& e : invocations) {
if (!is_suspicious[e[0]] && is_suspicious[e[1]]) {
// 无法移除可疑方法
vector<int> ans(n);
iota(ans.begin(), ans.end(), 0);
return ans;
}
}++
// 移除所有可疑方法
vector<int> ans;
for (int i = 0; i < n; i++) {
if (!is_suspicious[i]) {
ans.push_back(i);
}
}
return ans;
}
};
lc3578
其实是很典型 单调dq优化线性dp
半月前应该也刷到了一道类似的,第一时间还是没优化出来orz哭哭哭
class Solution {
public:
const static int N=5e4+10;
int f[N];
int countPartitions(vector<int>& nums, int k) {
int n=nums.size();
int mod=1e9+7;
f[0]=1;
deque<int> qx,qn;
int sum=0,l=0;
for(int i=0;i<n;i++){
while(!qx.empty()&&nums[i]>=nums[qx.back()]) qx.pop_back();
qx.push_back(i);
while(!qn.empty()&&nums[i]<=nums[qn.back()]) qn.pop_back();
qn.push_back(i);
sum=(sum+f[i])%mod;
while(!qx.empty()&&nums[qx.front()]-nums[qn.front()]>k){
if(l==qx.front()) qx.pop_front();
if(l==qn.front()) qn.pop_front();
sum=(sum+mod-f[l])%mod;
l++;
}
f[i+1]=sum;
}
// for(int i=0;i<=n;i++) cout<<f[i]<<' ';puts("");
return f[n];
}
};
deque优化划分dp
++idx 单调dq 维护窗口内的最大、最小值++,结合前缀和数组
++滑动窗口中统计元素差不超过k的合法分区数并取模++

class Solution {
public:
int countPartitions(vector<int>& nums, int k) {
const int MOD = 1'000'000'007;
int n = nums.size();
deque<int> min_q, max_q;
vector<int> f(n + 1);
f[0] = 1;
long long sum_f = 0; // 窗口中的 f[i] 之和
int left = 0;
for (int i = 0; i < n; i++) {
int x = nums[i];
++// 1. 入
sum_f += f[i];++
while (!min_q.empty() && x <= nums[min_q.back()])
min_q.pop_back();
min_q.push_back(i);
while (!max_q.empty() && x >= nums[max_q.back()]) {
max_q.pop_back();
}
max_q.push_back(i);
// 2. 出
++while (nums[max_q.front()] - nums[min_q.front()] > k) {++
++sum_f -= f[left];++
++left++;
if (min_q.front() < left)++
min_q.pop_front();
if (max_q.front() < left)
max_q.pop_front();
}
// 3. 更新答案
++f[i + 1] = sum_f % MOD;
}++
++return f[n];++
}
};
问题转化:要统计"所有元素差不超过K的子段分割方案数",可以用动态规划,设 ++f[i] 为前 i -1个元素的✓condition分割方案数++
滑动窗口找合法左边界:
用 multiset 维护窗口内的最大/最小值,通过双指针 j 找到每个右端点 i 对应的最小合法左边界(保证窗口内元素差≤K)
- 前缀和优化转移: f[i] 的方案数等于"前 i-1 个元素的所有合法前缀方案数之和",用前缀和++数组 g 快速计算这个区间和++ ,从而将转移时间复杂度优化到O(1)。
结合动态规划+滑动窗口+前缀和,就能高效解决子段分割计数问题啦~
class Solution {
public:
int countPartitions(vector<int>& nums, int K) {
int n = nums.size();
const int MOD = 1e9 + 7;
// f[i]:前 i 个元素的分割方案数
// g[i]:f 的前缀和
long long f[n + 1], g[n + 1];
f[0] = g[0] = 1;
// 用 multiset 记录滑动窗口里的数,方便求出最小值和最大值
multiset<int> ms;
// 枚举双指针的右端点 i,计算合法子段左端点的最小值 j
for (int i = 1, j = 1; i <= n; i++) {
ms.insert(nums[i - 1]);
while (j < i && *prev(ms.end()) - *ms.begin() > K) {
ms.erase(ms.find(nums[j - 1]));
j++;
}
// j 是最小的左端点
// 那么上一个子段最小的右端点就是 j - 1
// 前缀和就得减去 j - 2 的值
f[i] = (g[i - 1] - (j - 2 >= 0 ? g[j - 2] : 0) + MOD) % MOD;
g[i] = (g[i - 1] + f[i]) % MOD;
}
return f[n];
}
};
lc2762 不间断子数组
滑窗
class Solution {
public:
long long continuousSubarrays(vector<int>& nums) {
long long ans = 0;
map<int, int> cnt;
int left = 0;
for (int right = 0; right < nums.size(); right++) {
cnt[nums[right]]++;
while (cnt.rbegin()->first - cnt.begin()->first > 2) {
int y = nums[left];
if (--cnt[y] == 0) {
cnt.erase(y);
}
left++;
}
ans += right - left + 1;
}
return ans;
}
};
双堆法
class Solution {
public long continuousSubarrays(int[] nums) {
long res=0;
int left=0;
PriorityQueue<Integer> less=new PriorityQueue<>();//最小堆
PriorityQueue<Integer> more=new PriorityQueue<>((a,b)->(b-a));//最大堆
for(int i=0;i<nums.length;i++){
less.offer(nums[i]);
more.offer(nums[i]);
while(more.peek()-less.peek()>2){
more.remove(nums[left]);
less.remove(nums[left]);
left++;
}
res+=i-left+1;
}
return res;
}
}