基本概念
抽象数据类型(Abstract Data Type)
数据类型本身包含两个集合: 数据对象集合与相关联的操作(算法)集合. 其中数据对象集中数据对象之间的逻辑结构构成了数据结构的研究范畴.
抽象这一定语强调只关心数据类型中的数据对象集与相关操作集是什么而不涉及如果做到的问题。
粗略渐进符号系统
B
i
g
Big
Big
O
O
O与
L
i
t
t
l
e
Little
Little
o
o
o
同阶无穷小与高阶无穷小
递归定义:
f
(
n
)
=
O
(
1
)
:
=
t
h
e
r
e
e
x
i
s
t
s
C
s
.
t
.
f
(
n
)
C
→
1
f(n)=O(1) := there~exists~C~s.t.~\frac{f(n)}{C} \to1
f(n)=O(1):=there exists C s.t. Cf(n)→1
f
(
n
)
=
O
(
g
(
n
)
)
:
=
f
(
n
)
g
(
n
)
=
O
(
1
)
f(n)=O(g(n)) := \frac{f(n)}{g(n)}=O(1)
f(n)=O(g(n)):=g(n)f(n)=O(1)
read as
f
(
n
)
f(n)
f(n) is of the same order as
g
(
n
)
g(n)
g(n),
f
(
n
)
=
o
(
1
)
:
=
f
o
r
a
n
y
c
o
n
s
t
a
n
t
C
s
.
t
.
f
(
n
)
C
→
0
f(n)=o(1) := for~any~constant~C~s.t.~\frac{f(n)}{C} \to0
f(n)=o(1):=for any constant C s.t. Cf(n)→0
f
(
n
)
=
o
(
g
(
n
)
)
:
=
f
(
n
)
g
(
n
)
=
o
(
1
)
f(n)=o(g(n)) := \frac{f(n)}{g(n)}=o(1)
f(n)=o(g(n)):=g(n)f(n)=o(1)
read as
f
(
n
)
f(n)
f(n) is ultimately negligible compared to
g
(
n
)
g(n)
g(n).1
细分渐进符号系统
《算法导论》中有一个更精细的记法。
O
(
g
(
n
)
)
O(g(n))
O(g(n))不再表示
g
(
n
)
g(n)
g(n)是一个渐进紧确界,而使用
Θ
(
g
(
n
)
)
\Theta(g(n))
Θ(g(n))表示,
O
(
g
(
n
)
)
O(g(n))
O(g(n))被用来表示
g
(
n
)
g(n)
g(n)是一个渐进上界。记号表示一个处在特定范围内的函数集合,
f
(
n
)
=
Θ
(
g
(
n
)
)
f(n)=\Theta(g(n))
f(n)=Θ(g(n))这种习惯记法表示的实际是
f
(
n
)
∈
Θ
(
g
(
n
)
)
f(n) \in \Theta(g(n))
f(n)∈Θ(g(n))。
O
(
g
(
n
)
)
:
{
f
(
n
)
∣
f
(
n
)
<
c
g
(
n
)
,
∃
c
>
0
,
∃
N
>
0
,
∀
n
>
N
}
O(g(n)): \{f(n)|f(n)<cg(n), \exist c>0,\exist N>0,\forall n>N\}
O(g(n)):{f(n)∣f(n)<cg(n),∃c>0,∃N>0,∀n>N}
Ω ( g ( n ) ) : { f ( n ) ∣ c g ( n ) < f ( n ) , ∃ c > 0 , ∃ N > 0 , ∀ n > N } \Omega(g(n)): \{f(n)|cg(n)<f(n), \exist c>0,\exist N>0,\forall n>N\} Ω(g(n)):{f(n)∣cg(n)<f(n),∃c>0,∃N>0,∀n>N}
链表
单链表排序
插入排序 O ( n 2 ) O(n^2) O(n2)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(!head) return head;
ListNode sentinel(-1e5-1);
sentinel.next = head;
ListNode *predecessor=&sentinel, *cur=head, *successor=head->next;
predecessor->next=successor;
cur->next=nullptr;
while(true){
ListNode* head_it=&sentinel;
while(true){
if(head_it==predecessor){
cur->next=head_it->next;
head_it->next=cur;
predecessor=cur;
break;
}else if(head_it->next && head_it->next->val>=cur->val){
cur->next=head_it->next;
head_it->next = cur;
break;
}
head_it=head_it->next;
}
while (successor && predecessor->val<successor->val){
predecessor = successor;
successor = successor->next;
}
cur = successor;
if(!cur) break;
predecessor->next=cur->next;
successor = cur->next;
cur->next=nullptr;
}
return sentinel.next;
}
};
归并排序 T ( n ) = O ( n log ( n ) ) , S ( n ) = O ( 1 ) T(n)=O(n\log(n)), S(n)=O(1) T(n)=O(nlog(n)),S(n)=O(1)
由于是链表,无法随机访存,所以不能进行堆排序。
其次因为是单向链表,无法从两侧向中间移动指针,所以不能进行快速排序。
不同于数组,链表的插入不需要移动后续所有元素,没有拷贝消耗。因此原地更改是值得的。此归并使用迭代实现。
class Solution {
public:
pair<ListNode*,ListNode*> merge(ListNode* list1, ListNode *list2, int size){
int size1=size+1, size2=size;
ListNode *sentinel = new ListNode(-1e5-1);
ListNode *cur = sentinel, *successor = list1;
sentinel->next=list1;
while(cur&&list2&&size1&&size2){
while(size1&&successor&&successor->val<=list2->val){
cur=successor;
successor=cur->next;
size1--;
}
ListNode* data=list2;
list2=list2->next;
data->next=successor;
cur->next=data;
successor=cur->next;
size2--;
size1++;
}
while(size1-->1){
cur=cur->next;
}
cur->next=list2;
return {sentinel->next, cur};
}
ListNode* sortList(ListNode* head) {
if(!head) return head;
ListNode* sentinel = new ListNode(-1e5-1);
sentinel->next = head;
ListNode *cur=head, *successor=head->next;
int sub_length = 1;
ListNode* head0=head;
ListNode* head1=head0->next;
head0->next = nullptr;
ListNode *last=sentinel, *sub_last; // last用于链接归并后的链表, sub_last用归并前断开,产生两个子链表
while(true){
if (head0==sentinel->next&&!head1){
break;
}
auto front_tail = merge(head0, head1, sub_length);
last->next = front_tail.first;
last=front_tail.second;
head0 = last->next;
head1 = head0;
bool next_loop=false;
for(int i = 0; i < sub_length; i++){
if(!head1){
next_loop=true;
break;
}
sub_last = head1;
head1=head1->next;
}
if (next_loop){
sub_length*=2;
last=sentinel;
sub_last=nullptr;
head0=sentinel->next;
head1=head0;
for(int i = 0; i < sub_length; i++){
if(!head1){
break;
}
sub_last = head1;
head1=head1->next;
}
}
if (sub_last){
sub_last->next = nullptr;
}
}
return sentinel->next;
}
};
树
一般树
性质
规定根结点的层数为1, 使得与
二分查找判定树每个结点需要的查找次数刚好为该结点所在的层数
相一致. 2
定理1:
具有n个结点的完全二叉树的深度为 ⌊ log 2 n ⌋ + 1 \lfloor \log_{2}{n} \rfloor+1 ⌊log2n⌋+1
证明: 设深度为h, 对于完全二叉树可得n介于两个完美二叉树节点数之间 2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1}-1 < n \le 2^h-1 2h−1−1<n≤2h−1, 即
2
h
−
1
<
n
+
1
≤
2
h
(
1
)
⇒
2
h
−
1
≤
n
<
2
h
⇒
h
−
1
≤
log
2
n
<
h
⇒
log
2
n
<
h
≤
log
2
n
+
1
2^{h-1} < n+1 \le 2^h \quad (1)\\ \Rightarrow 2^{h-1} \le n < 2^h \\ \Rightarrow {h-1} \le \log_2{n} < h \\ \Rightarrow \log_2{n} < h \le \log_2{n}+1
2h−1<n+1≤2h(1)⇒2h−1≤n<2h⇒h−1≤log2n<h⇒log2n<h≤log2n+1
故
h
=
⌊
log
2
n
+
1
⌋
=
⌊
log
2
n
⌋
+
1
h=\lfloor \log_2{n}+1 \rfloor=\lfloor \log_2{n} \rfloor +1
h=⌊log2n+1⌋=⌊log2n⌋+1, 若对(1)式直接取对数则可得
h
=
⌈
log
2
(
n
+
1
)
⌉
h=\lceil \log_2{(n+1)} \rceil
h=⌈log2(n+1)⌉
定理1也等价于
一颗有n个元素的二叉树, n>0, 它的最小高度为 ⌊ log 2 n ⌋ + 1 = ⌈ log 2 ( n + 1 ) ⌉ \lfloor \log_{2}{n} \rfloor+1=\lceil \log_{2}{(n+1)} \rceil ⌊log2n⌋+1=⌈log2(n+1)⌉
表示方法
父结点表示法、孩子结点表示法(将所有孩子链接在一起)、父结点+孩子结点表示法
多数时候我们存储根节点来表征一颗树并且关心的是从根节点进行查找获得某个结点键对应的值或者判断该键是否存在于一个关联容器之中. 因此我们通常使用孩子结点表示法.
二叉树
二叉树是一种有序树, 因为由长子兄弟表示法每颗一般树都能与一颗二叉树一一对应, 因此研究二叉树性质即可. 且二分搜索技术需要二叉树结构支持.
种类
满树 full、完全树 complete、完美树 perfect
完全树的结点如果使用层次遍历且规定自左向右从根结点为1开始编号, 每个结点
i
i
i如果有左儿子的话编号为
2
i
2i
2i, 如果有右儿子的话编号为
2
i
+
1
2i+1
2i+1. 使得这种二叉树可以使用向量结构实现紧凑的存储和高效访问.
平衡树
AVL树
红黑树
单调栈
用于高效解决
O
(
n
)
O(n)
O(n)定位一个顺序容器中所有元素某一侧第一个更大或更小元素的问题
LeetCode496
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
vector<int> res(nums1.size(), -1);
unordered_map<int, int> query;
for (int i = 0; i < nums2.size(); i++) {
query.insert({nums2[i], i});
}
vector<int> larger(nums2.size(), -1);
stack<int> increacement;
for (int i = nums2.size() - 1; i >= 0; i--) {
int cur = nums2[i];
while (!increacement.empty() && increacement.top() < cur) {
increacement.pop();
}
larger[i] = increacement.empty() ? -1 : increacement.top();
increacement.push(cur);
}
for (int i = 0; i < nums1.size(); i++) {
res[i] = larger[query[nums1[i]]];
}
return res;
}
};
证明单调栈的正确性:
从右侧开始遍历,单调递增栈每次将顺序容器中第 i i i个元素 e i e_i ei入栈后,检查第 i − 1 i-1 i−1个元素 e i − 1 e_{i-1} ei−1与栈顶元素的大小。元素 e i − 1 e_{i-1} ei−1右侧第一个更大的元素要么是元素 e i e_i ei要么不是。
1.如果是第一种情况,将栈顶元素存入结果容器,这种情况显然正确。
2.如果是第二种情况,说明栈顶元素小于第 i − 1 i-1 i−1个元素,此时持续弹出直到单调递增(从栈顶到栈底)栈栈顶元素大于第 i − 1 i-1 i−1个元素。这个元素一定是右侧第一个大于第 i − 1 i-1 i−1的元素。因为右侧第一个大于第 i − 1 i-1 i−1的元素一定在栈内没有被弹出过。
反证法:假设正确结果 r r r已经被弹出,则一定有一个元素 e j e_j ej,在加入前大于元素 r r r导致元素 r r r被弹出,这说明存在一个元素 e j > r e_j>r ej>r,即 r r r不是 e i − 1 e_{i-1} ei−1右侧第一个更大的元素。假设错误,故正确结果未被弹出。
接雨水
分三种情况分别计算直接水池大小
超时,319 / 323 个通过的测试用例
bool le_(int a, int b) { return a <= b; }
bool less_(int a, int b) { return a < b; }
class Solution {
public:
vector<bool> selected_lf;
vector<bool> selected_ri;
vector<vector<int>> direct_traps;
vector<int> height;
int set_direct_traps(bool (*cmp)(int, int), vector<int> index) {
int res = 0;
int lo = 0, hi = 0;
int n = height.size();
pair<int, int> default_ = {-1, -1};
vector<pair<int, int>> larger(height.size(), default_);
stack<pair<int, int>> increacement; // index value
for (int i = 0; i < index.size(); ++i) {
int index_i = index[i];
int cur = height[index_i];
while (!increacement.empty() &&
cmp(increacement.top().second, cur)) {
increacement.pop();
}
larger[index_i] =
increacement.empty() ? default_ : increacement.top();
increacement.push({index_i, height[index_i]});
}
for (int i = 0; i < larger.size(); i++) {
if (larger[i] == default_) {
continue;
}
lo = i;
hi = larger[i].first;
if (lo > hi) {
std::swap(lo, hi);
}
int volume = 0;
int sea_level = height[lo] < height[hi] ? height[lo] : height[hi];
for (int j = lo + 1; j < hi; j++) {
volume += (sea_level - height[j]);
}
direct_traps[lo][hi] = volume;
}
return res;
}
int get_all() {
int res = 0;
int lo, hi;
int n = height.size();
for (lo = 0; lo < n; lo++) {
for (hi = n - 1; hi > lo; hi--) {
if ((!selected_lf[lo] || !selected_ri[hi]) &&
direct_traps[lo][hi]) {
std::fill(selected_lf.begin() + lo,
selected_lf.begin() + hi, true);
std::fill(selected_ri.begin() + lo + 1,
selected_ri.begin() + hi + 1, true);
res += direct_traps[lo][hi];
}
}
}
return res;
}
int trap(vector<int>& height) {
selected_lf = vector<bool>(height.size(), false);
selected_ri = vector<bool>(height.size(), false);
direct_traps =
vector<vector<int>>(height.size(), vector<int>(height.size(), 0));
this->height = height;
int res = 0;
vector<int> index(height.size(), 0);
for (int i = 0; i < index.size(); i++) {
index[i] = i;
}
set_direct_traps(le_, index);
for (int i = 0; i < index.size(); i++) {
index[i] = height.size() - i - 1;
}
set_direct_traps(le_, index);
set_direct_traps(less_, index);
res = get_all();
return res;
}
};
单调栈 O ( n ) O(n) O(n)版本
bool le_(int a, int b) { return a <= b; }
bool less_(int a, int b) { return a < b; }
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
stack<pair<int, int>> increacement; // index value
int n = height.size();
for (int i = 0; i < n; i++){
int cur = height[i];
while(!increacement.empty() && increacement.top().second<cur){
int bottom = increacement.top().second;
increacement.pop();
if (increacement.empty()){
continue;
}
int left_idx = increacement.top().first;
int left_height = increacement.top().second;
int right_idx = i;
int right_height = cur;
int sea_level = min(left_height, right_height);
int cur_trap = (sea_level-bottom)*(right_idx-left_idx-1);
res += cur_trap;
}
increacement.push({i, cur});
}
return res;
}
};
并查集
ADT
class UnionFind{
public:
vector<int> parent;
UnionFind(int n);
int find(int x);
void unite(int x, int y);
bool connected(int x, int y);
}
实现
UnionFind::UnionFind(int n){
parent = vector<int>(n, 0);
for (int i = 0; i < n; i++){
parent[i] = i;
}
}
int UnionFind::find(int x){
if (x == parent[x]){
return x;
}
parent[x] = find(parent[x]);
}
void UnionFind::unite(int x, int y){
int u = find(x);
int v = find(y);
parent[v] = u;
}
bool UnionFind::connected(int x, int y){
return find(x)==find(y);
}
我的代码,AC但是不够优
class UnionFind{
public:
vector<int> parent;
vector<vector<string>> merged_accounts;
UnionFind(int n){
parent = vector<int>(n, 0);
for (int i = 0; i < n; i++){
parent[i] = i;
}
}
void unite(int x, int y){
int u = find(x);
int v = find(y);
parent[v] = u;
}
bool connected(int x, int y){
return find(x) == find(y);
}
unordered_map<int, int> get_parent_idx(){
unordered_map<int, int> parent_idx;
int rank = 0;
for (int& p : parent){
if (parent_idx.find(p) == parent_idx.end()){
parent_idx.insert({p, rank++});
}
}
return parent_idx;
}
int find(int x){
if (x == parent[x]){
return x;
}
parent[x] = find(parent[x]);
return parent[x];
}
};
class Solution {
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
map<string, vector<int>> unique_names;
unique_names.insert({accounts[0][0], {0}});
for (int i = 1; i < accounts.size(); i++){
string name = accounts[i][0];
if (unique_names.find(name) == unique_names.end()){
unique_names.insert({name, {i}});
}else{
unique_names[name].push_back(i);
}
}
vector<vector<string>> res;
for (auto & name_idxs_ : unique_names){
vector<int> name_idxs = name_idxs_.second;
auto name = name_idxs_.first;
int n = 0;
for(int idx : name_idxs){
n += (accounts[idx].size()-1);
}
UnionFind union_find(n);
unordered_map<string, int> added;
int offset = 0;
for(int idx : name_idxs){
for (int i = 1; i < accounts[idx].size(); i++){
string email = accounts[idx][i];
union_find.unite(offset, offset+i-1);
if (added.find(email) != added.end()){
union_find.unite(union_find.find(added[email]), offset);
}
added.insert({email, offset+i-1});
}
offset += (accounts[idx].size()-1);
}
unordered_map<int,int> parent_idx = union_find.get_parent_idx();
vector<vector<string>> immediate_res(parent_idx.size(), vector<string>({name}));
offset = 0;
for(int idx : name_idxs){
int num_emails = accounts[idx].size()-1;
for (int i = 0; i < num_emails; i++){
string email = accounts[idx][i+1];
string last = immediate_res[parent_idx[union_find.find(offset+i)]].back();
if (last != email){
immediate_res[parent_idx[union_find.find(offset+i)]].push_back(email);
}
}
offset += num_emails;
}
for(auto& res_i : immediate_res){
if (res_i.size()==1){
continue;
}
sort(res_i.begin()+1, res_i.end());
string last = res_i[0];
res.push_back({last});
for (int i = 1; i < res_i.size(); i++){
string last = res.back().back();
if (last != res_i[i]){
res.back().push_back(res_i[i]);
}
}
}
}
return res;
}
};
优化点1:emplace_back()用来替代push_back()可以减少临时对象的创建与拷贝构造
优化点2:邮箱的idx直接用第一次出现的次序即可,不需要记录后再合并
优化点3:其实不需要offset,直接用一个每次自增的变量即可
为什么要用并查集:
给定 n n n个账户,每个账户 i i i有 m i m_i mi个邮箱。使用蛮力算法确定两个账户 i , j i, j i,j是否相同需要 O ( m i m j ) O(m_im_j) O(mimj)次邮箱比较,确定 N i N_i Ni个同名账户是否属于同一个人最坏需要 O ( N i ( N i − 1 ) 2 ) O(\frac{N_i (N_i-1)}{2}) O(2Ni(Ni−1))次账户比较,总的时间复杂度为 O ( ∑ n i = 1 N i ∑ n j = n i N i m n i m n j ) O(\sum_{n_i=1}^{N_i}\sum_{n_j=n_i}^{N_i}m_{n_i}m_{n_j}) O(∑ni=1Ni∑nj=niNimnimnj)。但是使用路径压缩的并查集的每次直接调用unite与find仅需 O ( α ( n ) ) O(\alpha(n)) O(α(n)), α \alpha α是阿克曼函数的逆函数。构造时最多 O ( ∑ i m i ) O(\sum_i m_i) O(∑imi)次unite操作。
二分查找
C++ STL中有两个算法lower_bound和upper_bound用来确定有序容器中元素所在左闭右开区间。
lower_bound返回第一个大于等于val的迭代器。
upper_bound返回第一个大于val的迭代器。
LeetCode704
存在返回下标,不存在返回-1:
class Solution {
public:
int search(vector<int>& nums, int target) {
int rank = -1;
int lo = 0, hi = nums.size();
int mi;
while(1<hi-lo){
mi = (lo+hi)/2;
if (nums[mi]<=target){
lo = mi;
}else{
hi = mi;
}
}
if (lo<hi){
rank = nums[lo]==target?lo:rank;
}
return rank;
}
};
返回大于等于target的第一个元素的下标,不存在则返回n:
class Solution {
public:
int search(vector<int>& nums, int target) {
int lo = 0, hi = nums.size();
int mi;
while(lo<hi){
mi = (lo+hi)/2;
if (nums[mi]<=target){
lo = mi;
if (hi-lo==1){
break;
}
}else{
hi = mi;
}
}
if (lo<hi && nums[lo]!=target){
return lo+1;
}else{
return lo;
}
}
};
返回小于等于target的第一个元素的下标,不存在则返回-1:
class Solution {
public:
int search(vector<int>& nums, int target) {
int lo = 0, hi = nums.size();
int mi;
while(lo<hi){
mi = (lo+hi)/2;
if (nums[mi]<=target){
lo = mi;
if (hi-lo==1){
break;
}
}else{
hi = mi;
}
}
if (lo==0 && nums[lo] != target){
return -1;
}else{
return lo;
}
}
};
排序
平均 | 最好 | 最坏 | 稳定性 | 额外空间 | 类型 |
---|---|---|---|---|---|
O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | 否 | O ( 1 ) O(1) O(1) | 选择 |
O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | 是 | O ( 1 ) O(1) O(1) | 冒泡 |
O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | 是 | O ( 1 ) O(1) O(1) | 插入 |
O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | 否 | O ( 1 ) O(1) O(1) | 堆 |
O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n 2 ) O(n^2) O(n2) | 否 | O ( 1 ) O(1) O(1) | 快速 |
O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | 是 | O ( n ) O(n) O(n) | 归并 |
亚 O ( n log ( n ) ) = O ( n + k ) O(n\log(n))=O(n+k) O(nlog(n))=O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | 是 | O ( n + k ) O(n+k) O(n+k) | 计数 |
亚 O ( n log ( n ) ) = O ( n + k ) O(n\log(n))=O(n+k) O(nlog(n))=O(n+k) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | 是 | O ( n + k ) O(n+k) O(n+k) | 桶 |
O ( n ⋅ k ) O(n \cdot k) O(n⋅k) | O ( n ⋅ k ) O(n \cdot k) O(n⋅k) | O ( n ⋅ k ) O(n \cdot k) O(n⋅k) | 是 | O ( n + k ) O(n + k) O(n+k) | 基数 |
O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | 否 | O ( 1 ) O(1) O(1) | 希尔 |
选择、冒泡、插入
选择
vector<int> selectionSort(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; i++){
int min_idx=i;
int min_val = nums[i];
for (int j = i+1; j < n; j++){
if (nums[j]<min_val){
min_idx = j;
min_val=nums[j];
}
}
swap(nums[i], nums[min_idx]); // 导致不稳定
}
return nums;
}
冒泡
vector<int> bubbleSort(vector<int>& nums) {
int n = nums.size();
bool sorted = false; // 终止指示
for (int i = 0; i < n; i++){ // 记录有序元素个数
sorted = true;
for (int j = 0; j < n-i-1; j++){
if (nums[j]>nums[j+1]){
swap(nums[j], nums[j+1]); // 可以使用tmp存储,在合适时插入
sorted = false;
}
}
if (sorted) break;
}
return nums;
}
插入
vector<int> insertionSort(vector<int>& nums) {
int n = nums.size();
for (int i = 1; i < n; i++){
int cur = nums[i];
int j = i-1;
for(; j >= 0; --j){
if (cur < nums[j]){
nums[j+1] = nums[j];
if (j==0) nums[j]=cur;
}else{
nums[j+1] = cur;
break;
}
}
}
return nums;
}
堆、快速、归并
堆
需要构造优先级队列时,额外增加push_heap即可。元素添加到末尾,然后沿着父亲链上滤。
template<typename Iter>
inline void filter_down(Iter first, int total, int cur){
while(true){
auto cur_it = first+cur-1;
int left_idx = total<cur*2?cur:cur*2;
int right_idx = total<cur*2+1?cur:cur*2+1;
auto left_child = first+left_idx-1;
auto right_child = first+right_idx-1;
int cur_val = *(first+cur-1);
if (cur_val>=*left_child && cur_val>=*right_child){break;}
int child_idx = *left_child>*right_child?cur*2:cur*2+1;
auto larger_it = first+child_idx-1;
swap(*larger_it, *cur_it);
cur = child_idx;
}
}
template<typename Iter>
void make_heap_(Iter first, Iter end){
size_t total = distance(first, end);
for(int idx = total; idx>0; idx--){
filter_down(first, total, idx);
}
}
template<typename Iter>
void pop_heap_(Iter first, Iter end){
int total = distance(first, end)-1;
swap(*first, *(end-1));
filter_down(first, total, 1);
}
class Solution {
public:
vector<int> heapSort(vector<int>& nums) {
size_t n = nums.size();
make_heap_(nums.begin(), nums.end());
for (size_t i = 0; i < n; i++){
pop_heap_(nums.begin(), nums.end()-i);
}
return nums;
}
};
快速
原地排序。对于堆排序来说优点在于可并行,使用不同物理核心处理不同部分。缺点是最坏情况的复杂度较高,可能被黑客利用进行拒绝服务攻击。
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
quickSort(nums, 0, nums.size()-1);
return nums;
}
void quickSort(vector<int>&nums, int lo, int hi){
if (lo>=hi) return;
int divider = partition(nums, lo, hi);
quickSort(nums, lo, divider-1);
quickSort(nums, divider+1, hi);
}
int partition(vector<int>&nums, int lo, int hi){
int pivot_idx = (lo+hi)/2; // 随机化pivot位置
swap(nums[pivot_idx], nums[hi]);
int& pivot = nums[hi];
lo -= 1;
while(lo<hi){
while(++lo<hi&&nums[lo]<pivot){}
while(lo<hi&&pivot<=nums[hi]){
hi--;
}
swap(nums[lo], nums[hi]);
}
swap(nums[lo], pivot);
return lo;
}
};
归并
优点有稳定,可以外排等。
空间未优化,
S
(
n
)
=
O
(
n
log
(
n
)
)
S(n)=O(n\log (n))
S(n)=O(nlog(n))
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
mergeSort(nums, 0, nums.size());
return nums;
}
void mergeSort(vector<int>& nums, int lo, int hi){
if (hi-lo==1) return;
int mid = (lo+hi)/2;
mergeSort(nums, lo, mid);
mergeSort(nums, mid, hi);
int n = hi-lo, offset=lo, sub_n1 = mid-lo;
vector<int> nums_cp(nums.begin()+lo, nums.begin()+hi);
int i = 0;
mid -= lo; lo = 0;
while(true){
if (lo>=sub_n1 || mid>=n){
break;
}
if (nums_cp[lo]<=nums_cp[mid]){
nums[offset+i++] = nums_cp[lo++];
}else{
nums[offset+i++] = nums_cp[mid++];
}
}
while(lo<sub_n1){
nums[offset+i++] = nums_cp[lo++];
}
while(mid<n){
nums[offset+i++] = nums_cp[mid++];
}
}
};
空间优化, S ( n ) = O ( n ) S(n)=O(n) S(n)=O(n)
class Solution {
public:
vector<int> nums_cp;
vector<int> sortArray(vector<int>& nums) {
nums_cp = vector<int>(nums);
mergeSort(nums, 0, nums.size());
return nums;
}
void mergeSort(vector<int>& nums, int lo, int hi){
if (hi-lo==1) return;
int mid = (lo+hi)/2;
mergeSort(nums, lo, mid);
mergeSort(nums, mid, hi);
int n = hi-lo, offset=lo, sub_n1 = mid-lo;
copy(nums.begin()+lo, nums.begin()+hi, nums_cp.begin());
// vector<int> nums_cp(nums.begin()+lo, nums.begin()+hi);
int i = 0;
mid -= lo; lo = 0;
while(true){
if (lo>=sub_n1 || mid>=n){
break;
}
if (nums_cp[lo]<=nums_cp[mid]){
nums[offset+i++] = nums_cp[lo++];
}else{
nums[offset+i++] = nums_cp[mid++];
}
}
while(lo<sub_n1){
nums[offset+i++] = nums_cp[lo++];
}
while(mid<n){
nums[offset+i++] = nums_cp[mid++];
}
}
};
数组的非递归归并排序
参考这位博主
子串长度每次翻倍,注意边界。
基数
希尔
计数
桶
堆
可用于优先级队列,堆排序等。
实现
建堆复杂度为
O
(
n
)
O(n)
O(n)
证明:假设是一颗高度为
h
h
h(根节点高度为1)的完全二叉树,比较次数和为:
S
(
h
)
=
2
h
−
1
+
2
h
−
2
⋅
2
+
⋯
+
2
0
⋅
h
1
2
S
(
h
)
=
2
h
−
2
+
2
h
−
3
⋅
2
+
⋯
+
2
−
1
⋅
h
1
2
S
(
h
)
=
2
h
−
1
+
2
h
−
2
+
⋯
+
2
0
−
2
−
1
⋅
h
1
2
S
(
h
)
=
2
h
−
1
−
2
−
1
⋅
h
1
2
S
(
h
)
=
n
−
2
−
1
⋅
h
S
(
h
)
=
2
n
−
h
=
O
(
n
)
S(h)=2^{h-1}+2^{h-2} \cdot 2+\cdots+2^0\cdot h \\ \frac{1}{2}S(h)=2^{h-2}+2^{h-3} \cdot 2+\cdots+2^{-1}\cdot h \\ \frac{1}{2}S(h)=2^{h-1}+2^{h-2}+\cdots+2^{0}-2^{-1}\cdot h \\ \frac{1}{2}S(h)=2^h-1-2^{-1}\cdot h \\ \frac{1}{2}S(h)=n-2^{-1}\cdot h \\ S(h)=2n-h=O(n)
S(h)=2h−1+2h−2⋅2+⋯+20⋅h21S(h)=2h−2+2h−3⋅2+⋯+2−1⋅h21S(h)=2h−1+2h−2+⋯+20−2−1⋅h21S(h)=2h−1−2−1⋅h21S(h)=n−2−1⋅hS(h)=2n−h=O(n)
第K大问题
堆实现有
O
(
n
+
k
log
(
n
)
)
O(n+k\log(n))
O(n+klog(n))和
O
(
n
log
(
k
)
)
O(n\log(k))
O(nlog(k))两种方案。
第一种使用大顶堆,先建堆,然后调用pop_heap()
k
k
k次。
第二种使用规模为
k
k
k的小顶堆。建堆,然后调用
n
−
k
n-k
n−k次push_heap()操作。算法执行结束后,堆内保存数组内较大的
k
k
k个元素。
k
k
k较小的时候都接近
O
(
n
)
O(n)
O(n)。随着
k
k
k增加,第一种在前期小,后期更快的趋近
n
log
(
n
)
n\log(n)
nlog(n)。
基于quickSort的pivot思想可以构建quickSelect。
quickSelect()
字符串匹配
KMP
动态规划
动态规划适用于没有更好算法的问题。这些问题只能蛮力穷举,但是还具有最优子问题性质,即穷举时会用到很多重复的子问题,这些子问题可以先行计算并存储以优化计算时间。
背包问题
01背包
二维DP
优点是可以通过回溯拿到最优方案对应的解
递推方程:
f
(
n
,
r
e
s
t
)
=
{
f
(
n
+
1
,
rest
)
,
rest
<
w
(
n
)
;
max
{
f
(
n
+
1
,
rest
)
,
f
(
n
+
1
,
rest
−
w
(
n
)
)
+
v
(
n
)
}
,
rest
≥
w
(
n
)
.
\begin{equation*} f(n, rest)= \begin{cases} f(n+1,\text{rest}),&\text{rest}<w(n);\\ \max \{f(n+1,\text{rest}), f(n+1,\text{rest}-w(n))+v(n)\},&\text{rest} \ge w(n). \end{cases} \end{equation*}
f(n,rest)={f(n+1,rest),max{f(n+1,rest),f(n+1,rest−w(n))+v(n)},rest<w(n);rest≥w(n).
以打表减少递归计算的动态规划初衷出发,我们可以写出以上递推方程。其中
n
n
n表示物品
n
n
n的决策阶段,rest表示这次决策时所剩背包资源容量。
首先创建dp表
import numpy as np
dp = np.zeros((N+1, bag+1)) # 物品0base计数,下标n表示所有决策进行后的边界。
首先对递归边界进行初始化,然后回归计算累加价值即可。
初始化
for rest in range(0, bag+1):
dp[n, rest] = 0 # 决策边界之外任何剩余空间都无法带来价值
回归计算
从二维递推关系中看出我们需要先计算所有
f
(
n
+
1
,
∗
)
f(n+1,*)
f(n+1,∗),再计算
f
(
n
,
∗
)
f(n,*)
f(n,∗),因此我们可以确定内层循环为rest,外层为n。
for n in range(N-1, -1, -1):
wn = w[n]
vn = v[n]
for rest in range(0, bag+1):
if rest<wn:
dp[n, rest] = dp[n+1, rest]
else:
dp[n, rest] = dp[n+1, rest] if dp[n+1, rest]>(dp[n+1, rest-wn]+vn) else dp[n+1, rest-wn]+vn
返回结果
print(dp[0, bag]) # n=0的决策结束后,dp表中存储的即为给定条件下的最大价值。
滚动dp优化打表维度
优点是可以空间复杂度更低。时间上也可以进行一定优化(不能优化渐进复杂度)。
通过观察递推公式与回归计算,可以发现
n
n
n这个维度可以只存在于循环控制中,在dp中通过时间而非空间存储。
因为内层循环为rest,dp[n, rest]的更新要么依赖dp[n, rest]要么依赖dp[n, rest-wn]。
而
r
e
s
t
≤
r
e
s
t
,
r
e
s
t
−
w
n
≤
r
e
s
t
rest \le rest, rest-wn \le rest
rest≤rest,rest−wn≤rest,即存在“单调性”。
因此我们在更新
d
p
[
n
,
r
e
s
t
i
]
dp[n, rest_i]
dp[n,resti]时只需要确保
d
p
[
n
−
1
,
r
e
s
t
j
]
dp[n-1, rest_j]
dp[n−1,restj],
j
≤
i
j \le i
j≤i都可被正确检索即可。
所以内层循环rest需要从bag向0步进,每次只更新最大的未更改值。
import numpy as np
dp = np.zeros((bag+1)) # 物品0base计数,下标n表示所有决策进行后的边界。
dp[rest] = 0 # 决策边界之外任何剩余空间都无法带来价值
for n in range(N-1, -1, -1):
wn = w[n]
vn = v[n]
for rest in range(bag, -1, -1):
if rest>=wn and dp[rest]<(dp[rest-wn]+vn):
dp[rest] = dp[rest-wn]+vn
print(dp[bag]) # n=0的决策结束后,dp表中存储的即为给定条件下的最大价值。
时间优化版
import numpy as np
dp = np.zeros((bag+1)) # 物品0base计数,下标n表示所有决策进行后的边界。
dp[rest] = 0 # 决策边界之外任何剩余空间都无法带来价值
for n in range(N-1, -1, -1):
wn = w[n]
vn = v[n]
for rest in range(bag, wn-1, -1):
if dp[rest]<(dp[rest-wn]+vn):
dp[rest] = dp[rest-wn]+vn
print(dp[bag]) # n=0的决策结束后,dp表中存储的即为给定条件下的最大价值。
网上多数的背包递推方程为:
f ( n , r e s t ) = { f ( n − 1 , rest ) , rest < w ( n ) ; max { f ( n − 1 , rest ) , f ( n − 1 , rest − w ( n ) ) + v ( n ) } , rest ≥ w ( n ) . \begin{equation*} f(n, rest)= \begin{cases} f(n-1,\text{rest}),&\text{rest}<w(n);\\ \max \{f(n-1,\text{rest}), f(n-1,\text{rest}-w(n))+v(n)\},&\text{rest} \ge w(n). \end{cases} \end{equation*} f(n,rest)={f(n−1,rest),max{f(n−1,rest),f(n−1,rest−w(n))+v(n)},rest<w(n);rest≥w(n).
他们的递推边界是f(0,rest),物品1base计数。以从边界向另一个方向推进的思想可以看出这两种方法是等价的。
区别在于可解释性中的决策方向。
假设物品摆放成一行后自左向右以0开始到N的编号。
我的n是自左向右决策时对第n件物品的决策。
传统公式可以看作自右向左进行的决策,这样回归时就是自左向右了,这时边界只能用0存储,因此他们使用1base。
蛮力递归 36 / 143 个通过的测试用例
class Solution {
public:
vector<int> nums;
bool recursive(int index, int bag){
if (bag<0) return false;
if(index>=nums.size()){
return bag==0;
}
bool plan0 = recursive(index+1, bag);
bool plan1 = recursive(index+1, bag-nums[index]);
return plan0 || plan1;
}
bool canPartition(vector<int>& nums) {
this->nums=nums;
int sum=0;
for(const int & num:nums){
sum += num;
}
if(sum%2) return false;
bool res = recursive(0, sum/2);
return res;
}
};
二维DP,且内层循环的边界不够优。 T ( n ) = O ( n ⋅ 1 2 ∑ i nums i ) T(n)=O(n \cdot \frac{1}{2} \sum_i \text{nums}_i) T(n)=O(n⋅21∑inumsi)(327ms)
bool canPartition(vector<int>& nums) {
int sum=0, n=nums.size();
sum = accumulate(nums.begin(), nums.end(), 0);
if(sum%2) return false;
int bag = sum/2;
vector<vector<bool>> memo(nums.size()+1, vector<bool>(bag+1,false));
memo[n][0]=true;
for(int index=n-1; index>=0; --index){
for(int rest=0; rest<=bag; ++rest){
int cur_weight = nums[index];
if(rest-cur_weight<0){
memo[index][rest]=memo[index+1][rest];
}else{
memo[index][rest]=memo[index+1][rest]||memo[index+1][rest-cur_weight];
}
}
}
return memo[0][bag];
}
一维DP,最终优化版 T ( n ) = O ( ∑ j ( 1 2 ∑ i nums i − nums j ) ) = O ( ( n − 1 ) ⋅ 1 2 ∑ i nums i ) T(n)=O(\sum_j(\frac{1}{2} \sum_i \text{nums}_i-\text{nums}_j))=O((n-1) \cdot \frac{1}{2} \sum_i \text{nums}_i) T(n)=O(∑j(21∑inumsi−numsj))=O((n−1)⋅21∑inumsi),运行时间更快(35ms),包括内层循环优化与节省内存分配时间。二维版没办法做内循环优化,因为他需要拷贝 n + 1 n+1 n+1次决策的结果来覆盖初始值。但是他的优点是可以通过回溯获取对应的解。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int bag = accumulate(nums.begin(), nums.end(), 0);
if(bag%2) return false;
bag /= 2;
vector<int> dp(bag+1, 0);
dp[0]=1;
for(int i = n-1; i >= 0; --i){
int wn = nums[i];
for(int rest=bag; rest>=wn; --rest){
if(dp[rest-wn]||dp[rest]){
dp[rest]=1;
}
}
}
return dp[bag];
}
};
多重背包
行展开成01背包即可
完全背包
每种物品的数量不设上限。
LeetCode279完全平方数
朴素三重循环
超时 570 / 589 个通过的测试用例
class Solution {
public:
int numSquares(int n) {
int i = 1;
vector<int> squares;
while(i*i<=n){
squares.push_back(i*i);
++i;
}
vector<vector<int>>dp(squares.size()+1, vector<int>(n+1, INT_MAX));
dp[squares.size()][0]=0;
for(i=squares.size()-1; i >= 0; --i){
int wn=squares[i];
for(int rest = 0; rest<=n; ++rest){
dp[i][rest]=dp[i+1][rest];
for(int k = 1; k*wn<=rest; ++k){
if(dp[i+1][rest-k*wn]!=INT_MAX&&dp[i+1][rest-k*wn]+k<dp[i][rest]){
dp[i][rest]=dp[i+1][rest-k*wn]+k;
}
}
}
}
return dp[0][n];
}
};
两重循环版
从以上代码中可以看到, rest − k ∗ w n < rest, k > 0 \text{rest}-k*wn<\text{rest},k>0 rest−k∗wn<rest,k>0。也就是当rest循环是从小到大时, d p [ i ] [ rest − w n ] dp[i][\text{rest}-wn] dp[i][rest−wn]其实已经更新完了,而且他自己需要检查所有 k k k更大的情况,并将结果保存到自己的位置,因此循环控制 k k k可以解除。
即 d p [ i ] [ rest − w n ] dp[i][\text{rest}-wn] dp[i][rest−wn]在更新的时候检查了
d p [ i + 1 ] [ rest − w n − k ⋅ w n ] + k , k ≥ 1 : = d p [ i + 1 ] [ rest − k ⋅ w n ] + k − 1 , k ≥ 0 dp[i+1][\text{rest}-wn-k \cdot wn]+k,k \ge 1:=dp[i+1][\text{rest}-k \cdot wn]+k-1, k \ge 0 dp[i+1][rest−wn−k⋅wn]+k,k≥1:=dp[i+1][rest−k⋅wn]+k−1,k≥0
并将其中最小值存储在自己的位置。而 d p [ i ] [ rest ] dp[i][\text{rest}] dp[i][rest]需要检查的
d p [ i + 1 ] [ rest − k ⋅ w n ] + k , k ≥ 0 dp[i+1][\text{rest}-k \cdot wn]+k, k \ge 0 dp[i+1][rest−k⋅wn]+k,k≥0,
除了 k = 0 k=0 k=0都已经减1比较过并存放在 d p [ i ] [ rest − w n ] dp[i][\text{rest}-wn] dp[i][rest−wn],因此只需要将 d p [ i ] [ rest − w n ] + 1 dp[i][\text{rest}-wn]+1 dp[i][rest−wn]+1即可获得至少选择1项时的最优解。
class Solution {
public:
int numSquares(int n) {
int i = 1;
vector<int> squares;
while(i*i<=n){
squares.push_back(i*i);
++i;
}
vector<vector<int>>dp(squares.size()+1, vector<int>(n+1, INT_MAX));
dp[squares.size()][0]=0;
for(i=squares.size()-1; i >= 0; --i){
int wn=squares[i];
for(int rest = 0; rest<=n; ++rest){
dp[i][rest]=dp[i+1][rest];
if(rest>=wn&&dp[i][rest-wn]!=INT_MAX&&dp[i][rest-wn]+1<dp[i][rest]){
dp[i][rest]=dp[i][rest-wn]+1;
}
}
}
return dp[0][n];
}
};
滚动DP版
class Solution {
public:
int numSquares(int n) {
int i = 1;
vector<int> squares;
while(i*i<=n){
squares.push_back(i*i);
++i;
}
vector<int>dp(n+1, INT_MAX);
dp[0]=0;
for(i=squares.size()-1; i >= 0; --i){
int wn=squares[i];
for(int rest = wn; rest<=n; ++rest){
if(dp[rest-wn]!=INT_MAX&&dp[rest-wn]+1<dp[rest]){
dp[rest]=dp[rest-wn]+1;
}
}
}
return dp[n];
}
};
LeetCode322零钱兑换
朴素三重循环+回溯法获取优化解
超时 115 / 189 个通过的测试用例
本题超时原因是回溯的复杂度而非三重循环。在满足题意的解足够多时,从回溯中获取最小硬币数的方法就会超时。
追踪数组记录递归树可以保存一个可行解,但是对解的额外约束无法满足。
因此本题需要直接以问题的目标作为优化函数而不是作为完全背包问题间接获取。
class Solution {
public:
vector<vector<int>> dp;
int n_coins=INT_MAX, n;
int coinChange(vector<int>& coins, int amount) {
n = coins.size();
dp = vector<vector<int>>(n+1, vector<int>(amount+1, 0));
for(int i = n-1; i>=0; --i){
int wn=coins[i]; int vn=coins[i];
for(int rest = 0; rest<=amount; ++rest){
dp[i][rest]=dp[i+1][rest];
for(int k = 1; k*wn<=rest; ++k){
if (dp[i][rest]<dp[i+1][rest-k*wn]+k*vn){
dp[i][rest]=dp[i+1][rest-k*wn]+k*vn;
}
}
}
}
if(dp[0][amount]!=amount){
return -1;
}
backtrace(coins, 0, 0, amount, amount);
return n_coins;
}
inline void backtrace(const vector<int>& coins, int i, int n_coins, int bag, int total){
if(total==0 && n_coins<this->n_coins){
this->n_coins = n_coins;
return;
}
if(i==n || bag<0){
return;
}
int wn=coins[i], vn=coins[i];
for(int k = 0; k*wn<=bag; k++){
if(dp[i+1][bag-k*wn]+k*vn==total){
backtrace(coins, i+1, n_coins+k, bag-k*wn, total-k*vn);
}
}
}
};
朴素三重循环+标记法获取优化解
由于优化目标此时是恰好装满,拿到的任意可行解并不满足用币最少的要求。此算法只展示如何通过标记数组拿到一个解的流程。
class Solution {
public:
vector<vector<int>> dp;
vector<vector<int>> mark;
int n_coins=INT_MAX, n;
int coinChange(vector<int>& coins, int amount) {
sort(coins.begin(), coins.end(), less<int>());
n = coins.size();
dp = vector<vector<int>>(n+1, vector<int>(amount+1, 0));
mark = vector<vector<int>>(n+1, vector<int>(amount+1,INT_MAX));
for(int rest = coins[n-1]; rest<=amount; ++rest){
mark[n-1][rest] = n-1;
}
for(int i = n-1; i>=0; --i){
int wn=coins[i]; int vn=coins[i];
for(int rest = 0; rest<=amount; ++rest){
dp[i][rest]=dp[i+1][rest];
mark[i][rest]=mark[i+1][rest];
for(int k = 1; k*wn<=rest; ++k){
if (dp[i][rest]<dp[i+1][rest-k*wn]+k*vn){
dp[i][rest]=dp[i+1][rest-k*wn]+k*vn;
mark[i][rest]=i;
}
}
}
}
if(!amount) return 0;
if(dp[0][amount]!=amount){
return -1;
}
n_coins = get_solution(mark, amount, coins);
return n_coins;
}
int get_solution(vector<vector<int>> &mark, int bag, vector<int>& coins){
vector<int>selected(mark.size(), 0);
for(int i = mark.size()-1; i>=0; --i){
int wi = coins[i];
while(mark[i][bag]==i){
selected[i]++;
bag-=wi;
}
}
int res = accumulate(selected.begin(), selected.end(), 0);
return res;
}
};
记忆化搜索
对于完全背包问题,也可采取记忆化搜索逐个选取的方案求解。区别在于无法确定循环次数。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 丢掉面值过大的硬币在某些情况下可以优化 但是本题样例没有明显提升
// sort(coins.begin(), coins.end());
// auto it = upper_bound(coins.begin(), coins.end(), amount);
// vector<int>coins_cp=coins;
// if(it != coins.end()){
// int num_candidate = distance(coins.begin(), it);
// coins_cp = vector<int>(num_candidate, 0);
// copy(coins.begin(), it, coins_cp.begin());
// }
// coins = coins_cp;
vector<int> dp(amount+1, INT_MAX);
dp[0]=0;
for (const int &coin:coins){
if(coin>=dp.size()) continue;
dp[coin]=1;
}
while(true){
bool better=false;
for(int rest = amount; rest>=0; --rest){
for (const int &coin:coins){
if(rest>coin&&dp[rest-coin]!=INT_MAX && dp[rest]>dp[rest-coin]+1){
better=true;
dp[rest] = dp[rest-coin]+1;
}
}
}
if(!better || dp[amount]!=INT_MAX) break;
}
if(dp[amount]==INT_MAX){
return -1;
}else{
return dp[amount];
}
}
};
DP版
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX);
dp[0]=0;
for (const int &coin:coins){
if(coin>=dp.size()) continue;
dp[coin]=1;
}
for(int i = coins.size()-1; i>=0; --i){
int wn = coins[i];
for(int rest=wn; rest<=amount; ++rest){
if(dp[rest-wn]!=INT_MAX && dp[rest-wn]+1<dp[rest]){
dp[rest]=dp[rest-wn]+1;
}
}
}
if(dp[amount]==INT_MAX){
return -1;
}else{
return dp[amount];
}
}
};
最大连续子列和问题
最长回文子串
最长公共子序列(LCS)
最长不下降子序列(LIS)
最长严格递增子序列长度
存在 O ( n 2 ) O(n^2) O(n2)与 O ( n log ( n ) ) O(n\log(n)) O(nlog(n))两种做法。
O
(
n
2
)
O(n^2)
O(n2)的思想就是找到
i
i
i元素前面更小且最长的元素
j
j
j,把那个元素的长度加一即可。
O
(
n
2
)
O(n^2)
O(n2)算法实现
class Solution {
public:
vector<int> memo;
int get_rank(int rank_pre, vector<int>& nums){
int i = rank_pre + 1;
int res = -1;
int length_max = 0;
while(rank_pre >= 0 ){
if (nums[rank_pre] < nums[i]){
if (memo[rank_pre] > length_max){
res = rank_pre;
length_max = memo[rank_pre];
}
}
rank_pre--;
}
return res;
}
int lengthOfLIS(vector<int>& nums) {
memo = vector<int>(nums.size(), 0);
memo[0] = 1;
for (int i = 1; i < nums.size(); i++){
int rank_pre = get_rank(i-1, nums);
if (rank_pre >= 0){
memo[i] = memo[rank_pre] + 1;
}else{
memo[i] = 1;
}
}
int res = 1;
for (int & length:memo){
res = length > res ? length : res;
}
return res;
}
};
动机:如果总能知道元素
e
i
e_i
ei之前的最长严格单调递增序列的最后一个元素
e
j
e_j
ej,那么我们就能根据
i
,
j
i,j
i,j大小判定序列长度是否能进一步增长。从贪心的角度,元素
e
j
e_j
ej尽可能小,序列长度更可能增长。
如何设计一个数据结构存储元素
e
j
e_j
ej?
当我们发现一个元素
e
i
e_i
ei比元素
e
j
e_j
ej小并且比元素
e
j
−
1
e_{j-1}
ej−1大的时候我们找到了更小的
e
j
e_j
ej,替换即可。
似乎我们需要存储的是遍历到此处时找到的最长严格单调递增序列。
但是上述贪心是存在瑕疵的,我们只能替换现有长度的最后一个元素,并保持前
i
−
1
i-1
i−1个元素为子序列前缀。但是最优解不一定在这个子空间中。
最优解的第
i
i
i个元素可能在已经存储了长度
n
>
i
n>i
n>i的第
k
k
k次比较时才出现。
为了保证最优解的第
i
i
i个元素一定被存储。我们真正需要存储的
f
f
f,需要满足
f
[
i
]
f[i]
f[i]是长度
i
+
1
i+1
i+1的已知严格单调递增序列的最后一个且最小的元素。
假设我们已经在第
k
k
k次比较时拥有了这样的结构,元素
e
k
e_k
ek要如何更新
f
f
f?
替换掉
f
f
f中比元素
e
k
e_k
ek大的第一个元素
e
l
e_l
el,不存在则直接附加到末尾。
证明这样做保持性质:
长度 i + 1 i+1 i+1的已知严格单调递增序列的前 i i i个元素加上元素 e k e_k ek还是一个严格单调递增序列。
f
f
f中的元素的更新会导致它可能不再是一个合法的最长严格单调递增序列。
但是它总是合法的序列末尾最小元素存储结构。
O
(
n
log
(
n
)
)
O(n\log(n))
O(nlog(n))算法实现
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> min_lasts;
for (const int &num : nums){
auto it = std::lower_bound(min_lasts.begin(), min_lasts.end(), num);
if (it == min_lasts.end()){
min_lasts.push_back(num);
}else{
*it = num;
}
}
return min_lasts.size();
}
};
俄罗斯套娃问题
这是二维的最长严格递增子序列
O(n^3) 超时 84 / 87 个通过的测试用例
使用排序,unordered_set记录剩余可选信封可以从79/87优化到84/87。
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](vector<int>&e0, vector<int>&e1)->bool{
if (e0[0]==e1[0]){
return e0[1]<e1[1];
}else{
return e0[0]<e1[0];
}
});
int num = envelopes.size();
vector<vector<int>> memo(num, vector<int>(num, 0));
for(int i = 0; i < num; i++){
memo[0][i]=1;
}
// rank_i enve_j
unordered_set<int> survivors;
for(int i = 0; i < num; i++){
survivors.insert(i);
}
int i=1;
for(; i < num; i++){
bool no_more=true;
for(const int& j : survivors){
for(int k = 0; k < j; k++){
if (memo[i-1][k]&&envelopes[k][0]<envelopes[j][0]&&envelopes[k][1]<envelopes[j][1]){
memo[i][j]=1;
no_more=false;
break;
}
}
}
unordered_set<int> survivors_cp(survivors);
for(const int& j:survivors_cp){
if(!memo[i][j]) survivors.erase(j);
}
if(no_more){
break;
}
}
return i;
}
};
排序 O ( n log ( n ) ) O(n\log(n)) O(nlog(n)),然后利用严格最长递增子序列长度算法 O ( n 2 ) O(n^2) O(n2)或者改进版 O ( n log ( n ) ) O(n\log(n)) O(nlog(n))。
改进版算法
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](vector<int>&e0, vector<int>&e1)->bool{
if (e0[0]==e1[0]){
return e0[1]>e1[1];
}else{
return e0[0]<e1[0];
}
});
int num = envelopes.size();
vector<int> min_last;
for(const auto &enve:envelopes){
int height = enve[1];
auto it = lower_bound(min_last.begin(), min_last.end(), height);
if(it == min_last.end()){
min_last.push_back(height);
}else{
*it = height;
}
}
return min_last.size();
}
};
三维实心物体堆叠最高高度问题
O ( n 3 ) O(n^3) O(n3)的算法可以解决求三维物体实心堆叠最大高度的问题。要求每个维度都小才能堆叠。
核心算法
int maxGifts(vector<vector<int>>& gifts) {
sort(gifts.begin(), gifts.end());
int num = gifts.size();
vector<vector<int>> memo(num, vector<int>(num, 0));
int max_height=0;
for(int i = 0; i < num; i++){
memo[0][i]=gifts[i][2];
max_height = memo[0][i]>max_height?memo[0][i]:max_height;
}
// rank_i enve_j
unordered_set<int> survivors;
for(int i = 0; i < num; i++){
survivors.insert(i);
}
for(int i=1; i < num; i++){
bool no_more=true;
for(const int& j : survivors){
for(int k = 0; k < j; k++){
if (memo[i-1][k]&&
gifts[k][0]<gifts[j][0]&&
gifts[k][1]<gifts[j][1]&&
gifts[k][2]<gifts[j][2]){
if(memo[i][j]<memo[i-1][k]+gifts[j][2]){
memo[i][j]=memo[i-1][k]+gifts[j][2];
no_more=false;
max_height = max_height<memo[i][j]?memo[i][j]:max_height;
}
}
}
}
unordered_set<int> survivors_cp(survivors);
for(const int& j:survivors_cp){
if(!memo[i][j]) survivors.erase(j);
}
if(no_more){
break;
}
}
return max_height;
}
完整算法
#include<bits/stdc++.h>
using namespace std;
namespace std{
template <>
struct less<vector<int>> {
bool operator()(const vector<int>& e0, const vector<int>& e1) const {
if(e0[0]<e1[0]){
return true;
}else if(e0[0]>e1[0]){
return false;
}else{
if(e0[1]<e1[1]){
return true;
}else if(e0[1]>e1[1]){
return false;
}else{
return e0[2]<e1[2];
}
}
}
};
}
class Solution {
public:
int maxGifts(vector<vector<int>>& gifts) {
sort(gifts.begin(), gifts.end());
int num = gifts.size();
vector<vector<int>> memo(num, vector<int>(num, 0));
int max_height=0;
for(int i = 0; i < num; i++){
memo[0][i]=gifts[i][2];
max_height = memo[0][i]>max_height?memo[0][i]:max_height;
}
// rank_i enve_j
unordered_set<int> survivors;
for(int i = 0; i < num; i++){
survivors.insert(i);
}
for(int i=1; i < num; i++){
bool no_more=true;
for(const int& j : survivors){
for(int k = 0; k < j; k++){
if (memo[i-1][k]&&
gifts[k][0]<gifts[j][0]&&
gifts[k][1]<gifts[j][1]&&
gifts[k][2]<gifts[j][2]){
if(memo[i][j]<memo[i-1][k]+gifts[j][2]){
memo[i][j]=memo[i-1][k]+gifts[j][2];
no_more=false;
max_height = max_height<memo[i][j]?memo[i][j]:max_height;
}
}
}
}
unordered_set<int> survivors_cp(survivors);
for(const int& j:survivors_cp){
if(!memo[i][j]) survivors.erase(j);
}
if(no_more){
break;
}
}
return max_height;
}
};
int main(){
string line;
std::getline(cin, line);
istringstream iss(line);
int n;
iss >> n;
vector<vector<int>> gifts(n,vector<int>(3,0));
for(int i = 0; i < n; i++){
std::getline(cin, line);
istringstream iss(line);
iss >> gifts[i][0] >> gifts[i][1] >> gifts[i][2];
}
Solution s;
auto res = s.maxGifts(gifts);
cout << res;
return 0;
}
区间DP
基站盈利问题
Memory limit exceeded 10/15 accepted
#include<bits/stdc++.h>
using namespace std;
int n, m;
int main(){
vector<vector<int>> memo;
string line;
std::getline(cin, line, '\n');
istringstream iss(line);
iss >> n;
iss.clear();
std::getline(cin, line, '\n');
iss.str(line);
iss >> m;
iss.clear();
memo = vector<vector<int>>(n+1, vector<int>(n+1, 0));
for(int i = 0; i < m; i++){
std::getline(cin, line, '\n');
iss.str(line);
int s_a, s_b, p;
iss >> s_a >> s_b >> p;
if(memo[s_a][s_b]<p){
memo[s_a][s_b]=p;
}
iss.clear();
}
for (int length=1; length<=n; length++){
for(int start=1; start+length-1<=n; start++){
int end=start+length-1;
for(int mid=start; mid<end; mid++){
if(memo[start][mid]+memo[mid+1][end]>memo[start][end]){
memo[start][end]=memo[start][mid]+memo[mid+1][end];
}
}
}
}
cout << memo[1][n];
return 0;
}
图论
以下只考虑简单图:两个节点之间同方向至多有一条边。
最短路径
单源最短路径
T
(
n
)
=
Θ
(
∣
E
∣
⋅
T
d
k
+
∣
V
∣
⋅
T
e
m
)
T(n)=\Theta(|E| \cdot T_{dk}+|V| \cdot T_{em})
T(n)=Θ(∣E∣⋅Tdk+∣V∣⋅Tem)
LeetCode743
朴素dijkstra算法
OSPF协议(链路状态算法)使用dijkstra算法。
松弛在一个可随机访问的数组里,
T
d
k
=
O
(
1
)
T_{dk}=O(1)
Tdk=O(1)。线性搜索并弹出最小节点所需时间复杂度为
T
e
m
=
O
(
∣
V
∣
)
T_{em}=O(|V|)
Tem=O(∣V∣)。
因此整个算法时间复杂度为
Θ
(
∣
V
∣
2
)
\Theta(|V|^2)
Θ(∣V∣2)。
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
int res = -1;
unordered_map<int, int> unvisited;
vector<int> dist(n+1, INT_MAX);
dist[k]=0;
vector<vector<pair<int,int>>> adjacent_link(n+1, vector<pair<int,int>>());
for(int i = 1; i <= n; i++){
if (i==k) continue;
unvisited.insert({i, INT_MAX});
}
for(const auto& edge:times){
adjacent_link[edge[0]].push_back({edge[1], edge[2]});
if(edge[0]==k){
unvisited[edge[1]]=edge[2];
dist[edge[1]]=edge[2];
}
}
while(!unvisited.empty()){
pair<int,int> cur = {-1, INT_MAX};
for(const auto& vertex:unvisited){
if(vertex.second<cur.second){
cur = vertex;
}
}
if(cur.second==INT_MAX){
return -1;
}
unvisited.erase(cur.first);
for(auto&adjacent:adjacent_link[cur.first]){
if(!unvisited.count(adjacent.first)) continue;
if(adjacent.second!=INT_MAX&&unvisited[adjacent.first]>adjacent.second+cur.second){
unvisited[adjacent.first]=adjacent.second+cur.second;
dist[adjacent.first] = unvisited[adjacent.first];
}
}
}
for(int i = 1; i <= n; i++){
res = dist[i]>res?dist[i]:res;
}
return res;
}
};
堆优化dijkstra算法
松弛需要在一个优先级队列里进行入队,弹出需要恢复队列结构,两者都是
O
(
log
(
∣
V
∣
)
)
O(\log(|V|))
O(log(∣V∣))
因此整个算法时间复杂度为
Θ
(
(
∣
V
∣
+
∣
E
∣
)
⋅
log
(
∣
V
∣
)
)
\Theta((|V|+|E|) \cdot \log(|V|))
Θ((∣V∣+∣E∣)⋅log(∣V∣))。
适合稀疏图、稠密图复杂性更高
Θ
(
∣
V
∣
2
⋅
log
(
∣
V
∣
)
)
\Theta(|V|^2 \cdot \log(|V|))
Θ(∣V∣2⋅log(∣V∣))。
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
int res = -1;
unordered_set<int> visited;
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
vector<int> dist(n+1, INT_MAX);
dist[k]=0;
vector<vector<pair<int,int>>> adjacent_link(n+1, vector<pair<int,int>>());
for(const auto& edge:times){
adjacent_link[edge[0]].push_back({edge[1], edge[2]});
}
pq.push({0,k});
while(!pq.empty()){
pair<int,int> cur = {-1, INT_MAX};
cur = pq.top(); pq.pop();
if(cur.second==INT_MAX){
return -1;
}
if(visited.count(cur.second)) continue;
for(auto&adjacent:adjacent_link[cur.second]){
if(adjacent.second!=INT_MAX&&dist[adjacent.first]>adjacent.second+cur.first){
dist[adjacent.first] = adjacent.second+cur.first;
pq.push({dist[adjacent.first], adjacent.first});
}
}
visited.insert(cur.second);
}
for(int i = 1; i <= n; i++){
res = dist[i]>res?dist[i]:res;
}
if(res==INT_MAX){
return -1;
}else{
return res;
}
}
};
有负环的最短路径 Bellman-Ford算法
Bellman-Ford方程:
D
i
s
(
s
,
t
)
=
min
v
∈
a
d
j
a
(
s
)
{
W
(
s
,
v
)
+
D
i
s
(
v
,
t
)
}
Dis(s,t)=\min_{v \in adja(s)}\{W(s,v)+Dis(v,t)\}
Dis(s,t)=minv∈adja(s){W(s,v)+Dis(v,t)}。
RIP协议(距离向量算法)使用Bellman-Ford方程。
未优化DP表维度版本:
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
int n_edges = times.size();
vector<vector<int>> memo(n + 1, vector<int>(n + 1, INT_MAX));
memo[k][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++){
memo[j][i]=memo[j][i-1];
}
for (int j = 1; j <= n_edges; j++) {
const auto& edge = times[j - 1];
int u = edge[0], v = edge[1], w = edge[2];
if (memo[u][i - 1] != INT_MAX) {
memo[v][i] = memo[v][i] <= (memo[u][i - 1] + w)
? memo[v][i]
: (memo[u][i - 1] + w);
}
}
}
int res = 0;
for (int i = 1; i <= n; i++) {
res = res > memo[i][n] ? res : memo[i][n];
}
return res == INT_MAX ? -1 : res;
}
};
优化DP表维度版本:
多数直觉上
2
2
2维的DP,如果不需要回溯给出具体方案,可以改为滚动DP。例如背包问题。
多源最短路径与有负权边 Floyd算法
可以处理负边但不能处理负环,复杂度为
O
(
∣
V
∣
3
)
O(|V|^3)
O(∣V∣3)。每次考虑以前
k
k
k个为中介点,对所有
i
,
j
i,j
i,j节点对进行松弛。矩阵上一次的值表示前
k
−
1
k-1
k−1个为中介点的结果,第k次只需比较是否使用节点
k
k
k的情况。使用DP思想。
对角线在松弛中非负即代表出现了负环。
用prev[][]存储最短路径树即可进行path的重构。prev[u][v]存储u~>v路径上的倒数第二个节点。
#include <algorithm>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_set>
#include <climits>
#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> grid;
int main(){
string line;
std::getline(cin, line);
istringstream iss(line);
int m, n;
vector<int>stations;
vector<int>communities;
iss >> m >> n;
grid = vector<vector<int>>(m, vector<int>(n, 0));
for(int i = 0; i < m; i++){
std::getline(cin, line);
istringstream iss(line);
for(int j = 0; j < n; j++){
iss >> grid[i][j];
if(grid[i][j]==0){
stations.push_back(i*n+j);
}
if(grid[i][j]==1){
communities.push_back(i*n+j);
}
}
}
int num_vertices=m*n;
vector<vector<int>> min_dist(num_vertices, vector<int>(num_vertices, INT_MAX));
for(int i = 0; i < num_vertices; i++){
for(int j = 0; j < num_vertices; j++){
int row_i=i/n, col_i=i%n, row_j=j/n, col_j=j%n;
if (i==j) {
min_dist[i][j]=0;
}else{
if(grid[row_i][col_i]!=-1&&grid[row_j][col_j]!=-1){
if((row_i==row_j&&abs(col_i-col_j)==1)||(abs(row_i-row_j)==1&&col_i==col_j)){
min_dist[i][j]=1;
min_dist[j][i]=1;
}
}
}
}
}
for(int k = 0; k < num_vertices; k++){
for(int i = 0; i < num_vertices; i++){
for(int j = 0; j < num_vertices; j++){
if(min_dist[i][k]==INT_MAX || min_dist[k][j]==INT_MAX){
continue;
}
int tmp = min_dist[i][k]+min_dist[k][j];
if(min_dist[i][j]>tmp){
min_dist[i][j]=tmp;
}
}
}
}
int res = 0;
for(const int &community:communities){
int length_path=INT_MAX;
for(const int &station:stations){
if(min_dist[community][station]<length_path){
length_path=min_dist[community][station];
}
}
if (length_path<INT_MAX){
res+=length_path;
}
}
cout << res;
return 0;
}
环检测算法
最小生成树
prim算法
小树长大
kruskal算法
合并森林