时间复杂度O(n),空间复杂度O(1)
定义:根据题意,只有满足A的数量<2且连续的L<3才能获得出勤奖励,因此可定义动态规划方程为
d p [ i ] [ j ] [ k ] , 0 ≤ i ≤ n , 0 ≤ j ≤ 1 , 0 ≤ k ≤ 2 dp[i][j][k],0\le i \le n, 0\le j \le 1, 0\le k \le 2 dp[i][j][k],0≤i≤n,0≤j≤1,0≤k≤2
表示前 i i i天中有 j j j个A且末尾有连续 k k k个L的合格方案。
初始化:第0天只有一种情况,0个A且0个L,即
KaTeX parse error: Expected 'EOF', got '&' at position 45: …[k] = 0, j\ne 0&̲and& k\ne 0
状态转移
第 i i i天状态为P时,末尾连续的L数量清零,A的数量不变
d p [ i ] [ j ] [ 0 ] = ∑ k d p [ i − 1 ] [ j ] [ k ] dp[i][j][0] = \sum_k dp[i - 1][j][k] dp[i][j][0]=k∑dp[i−1][j][k]
第 i i i天状态为L时,末尾连续的L数量+1,A的数量不变
d p [ i ] [ j ] [ k ] = d p [ i ] [ j ] [ k ] + d p [ i − 1 ] [ j ] [ k − 1 ] , 1 ≤ k ≤ 2 dp[i][j][k] = dp[i][j][k] + dp[i - 1][j][k - 1],1\le k \le 2 dp[i][j][k]=dp[i][j][k]+dp[i−1][j][k−1],1≤k≤2
第 i i i天状态为A时,末尾连续的L数量清零,A的数量+1
d p [ i ] [ 1 ] [ 0 ] = d p [ i ] [ 1 ] [ 0 ] + ∑ k d p [ i − 1 ] [ 0 ] [ k ] dp[i][1][0] = dp[i][1][0] + \sum_k dp[i - 1][0][k] dp[i][1][0]=dp[i][1][0]+k∑dp[i−1][0][k]
返回值
∑ j ∑ k d p [ n ] [ j ] [ k ] \sum_j\sum_k dp[n][j][k] j∑k∑dp[n][j][k]
由于第 i i i行之和第 i − 1 i-1 i−1行有关,所以空间复杂度可由 O ( n ) O(n) O(n)优化至 O ( 1 ) O(1) O(1)。
设从根到叶子为一条完整路径L,则本题就是要遍历所有路径上 s u m ( L [ i . . . j ] ) = = t a r g e t S u m sum(L[i...j]) == targetSum sum(L[i...j])==targetSum的数量,前缀和为常见的计算 s u m ( L [ i . . . j ] ) sum(L[i...j]) sum(L[i...j])的方式,在此用哈希表hashMap
记录前缀和和前缀和出现的次数,并初始化hashMap[0] = 1
。而考虑到要遍历所有可能路径,则采用回溯的方式。回溯套路:
终止条件:当node为空时,返回0,表示不存在路径
更新
更新前缀和curSum += root->val
初始化本层的答案ans = 0
在前缀和哈希表中查找curSum - targetSum
,即查找是否存在curSum - sum = targetSum
的sum
值,若存在,则ans += hashMap[curSum - targetSum]
回溯
记录本层前缀和hashMap[curSum]++
ans加上左右子树的回溯递归值
去掉本层前缀和hashMap[curSum]--
时间复杂度 O ( s i z e ( e d g e s ) × n ) O(size(edges )\times n) O(size(edges)×n)
定义: d p [ i ] [ j ] dp[i][j] dp[i][j]表示由src经过 i i i次飞行到达 j j j的花费, 0 ≤ i ≤ k + 1 0\le i\le k + 1 0≤i≤k+1
初始化:$dp[i][j] = $ INT_MAX
状态转移:设from = edges[0]; to = edges[1]; cost = edges[2]
,遍历edges
数组,则有
d p [ i ] [ t o ] = min ( d p [ i ] [ t o ] , d p [ i − 1 ] [ f r o m ] + c o s t ) dp[i][to] = \min(dp[i][to], dp[i-1][from] + cost) dp[i][to]=min(dp[i][to],dp[i−1][from]+cost)
返回值: min i ( d p [ i ] [ d s t ] ) \min_i(dp[i][dst]) mini(dp[i][dst])
由于 d p [ i ] dp[i] dp[i]只和 d p [ i − 1 ] dp[i-1] dp[i−1]有关,可节约空间至 O ( 1 ) O(1) O(1)
//两个&&,因为flights类型是vector>
for (auto &&flight : flights) {...}
//move函数的使用
vector<int> cur(n, maxVal);
vector<int> next(n, maxVal);
cur = move(next);
差分属于前缀和的变种,前缀和解决了任意连续区间内所有元素之和/之积···运算,公式为 P [ j ] − P [ i ] = x i + . . . + x j − 1 , P [ i ] = x 0 + . . . + x i = P [ i − 1 ] + x i P[j]-P[i]=x_i+...+x_{j-1},P[i]=x_0+...+x_{i}=P[i-1]+x_i P[j]−P[i]=xi+...+xj−1,P[i]=x0+...+xi=P[i−1]+xi。差分公式为 D P [ i ] = x [ i ] − x [ i − 1 ] DP[i]=x[i]-x[i-1] DP[i]=x[i]−x[i−1],与前缀和的关系在于,差分数组的前缀和为数组本身。
若要对某一连续区间 [ x i , . . . , x j ] [x_i,...,x_j] [xi,...,xj]内的值增加某一相同值 y y y,反应在差分数组上就是 D P ′ [ i ] = D P [ i ] + y DP'[i]=DP[i]+y DP′[i]=DP[i]+y以及 D P ′ [ j + 1 ] = D P [ j + 1 ] − y DP'[j+1]=DP[j+1]-y DP′[j+1]=DP[j+1]−y
图的dfs遍历,类似于回溯算法,区别在于判断在循环外,因为要对第一个遍历点进行操作。要求课程的学习顺序就是要求图的一个可能的拓扑排序,将无环图视作一种起点为根的多叉树,那么拓扑排序=后序遍历图+逆转返回数组。
// 图的遍历模板,注意从每个点出发有多条遍历路径
// on_path: 是否在当前遍历的路径上
// visited: 是否有路径遍历过该点
vector<int> ans;
void dfs(graph, index, on_path, visited, cur_path) {
if (onPath[index]) {
...; // 出现环
return;
}
if (visited[index]) {
...;
return; // 已经被遍历过,通常直接返回防止重复
}
on_path[index] = true; // 记录路径,cur_path.emplace_back(index)
visited[index] = true;
...; // 前序遍历操作
for (next_node : graph[index]) {
// 遍历相邻的下一点
dfs(graph, next_node, on_path, visited);
}
...; // 后序遍历操作
ans.emplace_back(next_node); // 拓扑排序
on_path[index] = false; // 当前路径遍历完毕,cur_path.pop_back(index)
}
int main() {
for (auto &i : nodes) {
// 对每个点执行dfs
dfs(graph, i, on_path, visited);
}
...; // 后序操作
}
743. 网络延迟时间 - 力扣(LeetCode) (leetcode-cn.com)
1631. 最小体力消耗路径 - 力扣(LeetCode) (leetcode-cn.com)
无向图、有向图均能用,只要能够将题目抽象为图,再将最优目标抽象为路径上权值计算。
// 假设起点为start,终点为end
// 构建邻接表graph[i]: 第i个节点的邻居vector,vector中每个元素为[邻居节点,路线权值]
vector<vector<pair<int, T>>> graph(n);
for (int i = 0; i < n; ++i) {
// 注意点1:empalce不用make_pair
graph[edges[i][0]].emplace_back(edges[i][1], weight[i]);
graph[edges[i][1]].emplace_back(edges[i][0], weight[i]); // 如果为无向图
}
vector<bool> visited(n, false); // 如果为无向图或有向有环图,防重复访问节点
vector<T> minPath(n, 0); // 从start到某个点的最小路径值
minPath[start] = 0; // 初始化:到自身的路径长度为0
// 存储<节点,当前最小路径>,按权重从大到小排列
// 注意priority_queue存储pair的默认方式是按照first从大到小
priority_queue<pair<U, int>> pq;
pq.emplace(0, start); // 初始化:到自身的路径长度为0
while (!pq.empty()) {
// 开始遍历图
// 注意点2:可以直接auto赋值
auto [curPath, curNode] = pq.top();
pq.pop();
if (curPath > minPath[curNode] || visited[curNode]) {
// 如果已经有更短路径或者已经访问过该点
continue;
}
if (curNode == end) {
// 如果为无向图或有向有环图,到达终点直接退出
break;
}
// 如果为无向图或有向有环图,防重复访问节点
visited[curNode] = true;
for (auto &[next, dist]: graph[curNode]) {
U distToNext = dist + curPath;
if (distToNext < minPath[next]) {
// 出现了更短路径,更新
minPath[next] = distToNext;
pq.empalce(distToNext, next);
}
}
}
return minPath[end];
时间复杂度O(nlog n),空间复杂度O(n)
class Solution {
public:
ListNode *findMidNode(ListNode *start, ListNode *end) {
// 快慢指针找中间节点
ListNode *fast = start, *slow = start;
while (fast != end && fast->next != end) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
TreeNode* helper(ListNode *l, ListNode *r) {
// 分治
if (l == r) {
return nullptr;
}
ListNode *midNode = findMidNode(l, r);
TreeNode *root = new TreeNode(midNode->val);
root->left = helper(l, midNode);
root->right = helper(midNode->next, r);
return root;
}
TreeNode* sortedListToBST(ListNode* head) {
return helper(head, nullptr);
}
};
时间复杂度O(n)
已知对二叉搜索树中序遍历的结果就是链表顺序,因此也可通过顺序遍历链表中序构造二叉搜索树。同时设有左右端点index,即[l, r],方便判断当前节点是否为nullptr。
class Solution {
public:
int getLen(ListNode* head) {
// 获取链表长度
int len = 0;
while (head) {
++len;
head = head->next;
}
return len;
}
TreeNode* helper(ListNode*& node, int l, int r) {
// 为了判断当前节点是否为nullptr
if (l > r) {
return nullptr;
}
// 顺序遍历,中序构造
// 注意参数为指针的引用,所以递归会改变指针指向
int mid = (1 + l + r) / 2; // 奇数时取下界
TreeNode *root = new TreeNode();
root->left = helper(node, l, mid - 1);
root->val = node->val;
++node;
root->right = helper(node, mid + 1, r);
return root;
}
TreeNode* sortedListToBST(ListNode* head) {
return helper(head, 0, getLen(head) - 1);
}
};
原始思路:对A升序排序,找到满足大于B[i]的最小A[i]值,如果找不到,就用A[i]的最小值顶替
问题:由于A是vector,不管是直接删除元素还是伪删除都需要大量时间,会导致超时
解决:将B降序排序为B_sort,遍历B_sort,每次拿A.back()
即A的最大值跟B_sort[i]比较,如果比得过加入答案数组,比不过拿最小值顶替。但由于题目要求要按找未排序B的顺序返回答案,此时如果说明无重复元素可用unordered_map,可能有重复元素则用存储pair
的优先队列(堆)。注意pair比较默认拿第一个值比,优先队列默认为大根堆,所以存储pair的优先队列以第一个值的降序排列。
此外还有一个好处,因为每次比较的都是A的尾,删除的要不是A的头要不是A的尾,所以可以采用双指针指向头尾进行伪删除,避免pop_back或erase造成开销。
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size();
int l = 0;
int r = n - 1;
priority_queue<int, int> B_sort; // 存储并为val的大根堆
vector<int> ans;
// 构造堆,相当于对B降序排列
for (int i = 0; i < n; ++i) {
B_sort.emplace(nums2[i], i);
}
// 遍历B_sort
while(!B_sort.empty()) {
int val = B_sort.top().first;
int idx = B_sort.top().second;
B_sort.pop();
// 拿B_sort当前最大值跟A最大值比较
if (A[r] > val) {
// 比得过,答案为A最大值
ans.emplace_back(A[r--]);
} else {
// 比不过,拿最小值顶替
ans.emplace_back(A[l++]);
}
}
return ans;
}
唯一的问题是获取随机元素,先获取[0, set.size() - 1]的随机数n,然后返回*(set.begin() + n)
。但这么做获取随机数的事件还是过长
class RandomizedSet {
public:
RandomizedSet() {
}
bool insert(int val) {
auto iter = data.find(val);
if (iter == data.end()) {
data.insert(val);
return true;
}
return false;
}
bool remove(int val) {
auto iter = data.find(val);
if (iter == data.end()) {
return false;
}
data.erase(iter);
return true;
}
int getRandom() {
int random_idx = random() % data.size();
auto iter = data.begin();
while (random_idx--) {
++iter;
}
return *iter;
}
private:
unordered_set<int> data;
};
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet* obj = new RandomizedSet();
* bool param_1 = obj->insert(val);
* bool param_2 = obj->remove(val);
* int param_3 = obj->getRandom();
*/
数组满足以O(1)时间取随机数和末尾插入元素的要求,但却无法满足O(1)删除指定元素。考虑为什么无法以O(1)删除?是因为数组有一个查找指定元素下标,以及删除后向前移动元素填补空缺的过程。
对于查找指定下标,可以用哈希map存储<元素,对应下标>解决;对于向前移动元素,由于题目对数组存储顺序并无要求,所以我们每次将待删除的元素换到末尾,更新map,最后pop_back()
并从map中删除指定项。
class RandomizedSet {
public:
RandomizedSet() {
}
bool insert(int val) {
if (data_idx.find(val) == data_idx.end()) {
data.emplace_back(val);
data_idx[val] = data.size() - 1;
return true;
}
return false;
}
bool remove(int val) {
auto iter = data_idx.find(val);
if (iter == data_idx.end()) {
return false; // 不存在指定元素
}
int idx = iter->second; // 获取待删除的下标
data[data.back()] = idx; // 更新末尾元素下标
swap(data[idx], data.back()); // 将待删除元素交换到末尾
// 删除指定元素
data.pop_back();
data_idx.erase(iter);
return true;
}
int getRandom() {
int random_idx = random() % data.size();
return data[random_idx];
}
private:
unordered_map<int, int> data_idx;
vector<int> data;
};
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet* obj = new RandomizedSet();
* bool param_1 = obj->insert(val);
* bool param_2 = obj->remove(val);
* int param_3 = obj->getRandom();
*/
一个长为 n n n的数组,最差的情况是存储[1,…,n]所有数,这时候答案为n+1,其他情况下答案一定在[1,n]范围内。
原地哈希记住nums[nums[i] + k] = nums[i]
的公式及其变种,本题公式为nums[nums[i] - 1] = nums[i]
,目的在于还原正数的位置(因为存在n所以需要-1)。还原之后再次遍历就能找出缺失的第一个正数
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; ++i) {
// 如果nums[i]在[1,n]范围内
// 就将其放到应该存在的位置上,即nums[nums[i] - 1] = nums[i]
// 为了避免原来nums[nums[i] - 1](新nums[i])上的数被覆盖
// 要继续判断并交换,直到无法再调整,即nums[nums[i] - 1] == nums[i]或nums[i]不在范围内
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
swap(nums[i], nums[nums[i] - 1]);
}
}
for (int i = 0; i < n; ++i) {
// 现在,所有[1,n]范围内的数i都在下标i-1的位置上
// 那么找到第一个不满足条件的i即为答案
if (i + 1 != nums[i]) {
return i + 1;
}
}
return n + 1;
}
验证匹配类题目不能总想着面向用例编程,一遍遍查缺补漏。要有一套完整的逻辑流程。
JudgeIPv6
进行判断,否则进入JudgeIPv4
判断。这里要注意分割出来的子字符串为空不能加入字符串数组