排序
快速排序
——分治
1.找分界点
左边界 中间值 右边界
2.区间分成两部分(重点
把整个区间根据x的值分成两部分(重点,让小于等于x的在一边,大于的在另一边
3.递归
对左右两端做同样处理
核心步骤:
1.暴力做法
设置两个数组,将小于分界点的数放入一个数组,大于等于分界点的数放入另一个数组, 再将两个数组中的数分别放入需要排序的数组中(空间复杂度较高
2.常规
此处以从小到大排序为例:
左右两边各设置一个指针i,j
左边指针向右走,直到遇到大于分界线的数,右边指针向左走,直到遇到小于分界线的数,然后将两个数交换
重复以上步骤,直到i,j相遇,此时左右两边已经被分成了两个部分
注:边界问题
模板:
void quick_sort(int q[], int l, int r) { if (l >= r) return; int i = l - 1, j = r + 1, x = q[l + r >> 1]; while (i < j) { do i ++ ; while (q[i] < x); do j -- ; while (q[j] > x); if (i < j) swap(q[i], q[j]); } quick_sort(q, l, j), quick_sort(q, j + 1, r); }
归并排序
——双指针算法
1.找分界点
和快速排序不同,归并排序基本上就是分为左右两部分,但是从理论上而言也是可以随机取一个分界点的
2.递归排序
对前面一半和后面一半递归排序
3.排序
依旧是两个指针,分别指向前后两个部分的开头数字,然后开始向后移动,将所指向的两个数中小(大)的那个数放入新数组中,然后指针后移,重复这一步骤,直到某一部分指针指到最后,最后将剩余部分拷贝到新数组并将新数组的全部数据拷贝到目标数组中
模板:
void merge_sort(int q[], int l, int r) { if (l >= r) return; int mid = l + r >> 1; merge_sort(q, l, mid); merge_sort(q, mid + 1, r); int k = 0, i = l, j = mid + 1; while (i <= mid && j <= r) if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ]; else tmp[k ++ ] = q[j ++ ]; while (i <= mid) tmp[k ++ ] = q[i ++ ]; while (j <= r) tmp[k ++ ] = q[j ++ ]; for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j]; }
总结:
总的而言这两个算法十分相似,区别只是,一个是无序到逐渐有序的过程,另外一个则是区间有序然后逐步调整为整体有序
思想和实现上都是运用了递归的思想,相似度较高
二分
对二分的解读:
最开始介绍的时候一般说的是存储位置连续且数据单调的数组可以用二分查找法。但是其实是不对的,二分的本质不是单调二是边界。(单调一定可以二分,但不单调也并不一定不能二分
在区间上定义一种性质,使整个区间可以分成两部分,左半边满足而右半边不满足,那么就可以找到关于这种性质的边界
同时关于二分的问题,保证区间内一定有答案,二分的时候一定有解(题目可能无解,定义的性质一定是有边界的,算法一定可以将其二分出来
整数二分
模板:
这里有两个模板,分别适用于不同的情况,核心的区别就是求 mid 的时候是否要加一
bool check(int x) {/* ... */} // 检查x是否满足某种性质 // 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用: int bsearch_1(int l, int r) { while (l < r) { int mid = l + r >> 1; if (check(mid)) r = mid;// check()判断mid是否满足性质 else l = mid + 1; } return l; } // 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用: int bsearch_2(int l, int r) { while (l < r) { int mid = l + r + 1 >> 1; if (check(mid)) l = mid; else r = mid - 1; } return l; }
实数二分
浮点数二分的本质也是边界问题,但是相比起整数二分,实数二分每次一定可以严格缩小一半,相对而言简单很多,当区间长度足够小的时候,就可以确定以l或者r为解
关于区间长度的判定有一个技巧,例如题目要求保留六位小数,那么区间长度取 1e-8 的话就可以有效避免准确度的问题,一下子取到准确的题目要求的结果
模板:
bool check(double x) {/* ... */} // 检查x是否满足某种性质 double bsearch_3(double l, double r) { const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求 while (r - l > eps) { double mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } return l; }
前缀和与差分
前缀和
一维前缀和
S[i] = a[1] + a[2] + ... a[i] a[l] + ... + a[r] = S[r] - S[l - 1]
//
二维前缀和
//S[i, j] = 第i行j列格子左上部分所有元素的和 //以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
对前缀和的解释,比如给一个数组a,然后用s来存储a的累加和,例如si就是从a1到ai的和,这种算法叫做前缀和,通过前缀和算法能快速确定数组中的特定两个数据之间的和
差分
差分是什么? 可以看作前缀和的逆运算(就是每一项与前一项做差) 举例说明,a是原数组,b是差分数组,那么前n项b数组的值相加得到a[n]
一维差分
例如:给区间[l, r]中的每个数加上c:
B[l] += c, B[r + 1] -= c
二维差分
例如:给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
类似于给一个数组a,然后差分数组b的前缀和就是a,也就是bi的值就是bi - b(i - 1)
双指针
双指针问题往往是滑动窗口问题,一般根据题目实际求值
滑动窗口
滑动窗口,可以用来解决一些查找满足一定条件的连续区间的性质(长度等)的问题。由于区间连续,因此当区间发生变化时,可以通过旧有的计算结果对搜索空间进行剪枝,这样便减少了重复计算,降低了时间复杂度。往往类似于“请找到满足xx的最x的区间(子串、子数组)的xx”这类问题都可以使用该方法进行解决。 一般滑动窗口维护两个指针,左指针和右指针。 当窗口内的元素未达到题目条件时,右指针右移,探索未知的区间来满足条件 当窗口内的元素达到题目条件时,左指针右移,压缩区间,使窗口尽可能短得满足题目条件 这里给出一个滑动窗口的基本模板
int slidingWindow(vector<int> nums) { int n = nums.size(); int ans = 0; // 记录窗口内的元素及其个数,非必要 map<int, int> um; // l:窗口左边界; r:窗口右边界 int l = 0, r = 0; // r 指针负责探索新的区间,直到搜索到nums的某末尾 while (r < n) { um[r]++; // 如果区间不满足条件,l指针右移,窗口收缩 while(check(l, r)) {//区间l,r满足条件时 um[l]--; l++; } // 此处处理结果, deal with(ans, 区间[l, r]) res = max(ans, r - l + 1); // 或者res = min(ans, r - l + 1); // 右指针右移,继续搜索 r++; } return ans; }
//力扣 一个双指针的滑动窗口问题
高精度
高精度加法
// C = A + B, A >= 0, B >= 0 vector<int> add(vector<int> &A, vector<int> &B) { if (A.size() < B.size()) return add(B, A); vector<int> C; int t = 0; for (int i = 0; i < A.size(); i ++ ) { t += A[i]; if (i < B.size()) t += B[i]; C.push_back(t % 10); t /= 10; } if (t) C.push_back(t); return C; }
高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0 vector<int> sub(vector<int> &A, vector<int> &B) { vector<int> C; for (int i = 0, t = 0; i < A.size(); i ++ ) { t = A[i] - t; if (i < B.size()) t -= B[i]; C.push_back((t + 10) % 10); if (t < 0) t = 1; else t = 0; } while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }
高精度乘低精度
// C = A * b, A >= 0, b >= 0 vector<int> mul(vector<int> &A, int b) { vector<int> C; int t = 0; for (int i = 0; i < A.size() || t; i ++ ) { if (i < A.size()) t += A[i] * b; C.push_back(t % 10); t /= 10; } while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }
高精度除低精度
// A / b = C ... r, A >= 0, b > 0 vector<int> div(vector<int> &A, int b, int &r) { vector<int> C; r = 0; for (int i = A.size() - 1; i >= 0; i -- ) { r = r * 10 + A[i]; C.push_back(r / b); r %= b; } reverse(C.begin(), C.end()); while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }
逆元
(a*b)%p=1,那么b可以近似看成a^-1 注意:a,p互质
费马小定理求逆元(快速幂)
求a^(p-2)
int p = 1e8; //b = p - 2; int qmi(int a, int b)//a的b次方对p求余 { int ans = 1; while (b) { if (b % 1) ans = (ans * a) % mod; b >>= 1; a = a * a % mode; } return res; }//注意乘法运算的过程中可能越界,所以最好使用long long
扩展欧几里得算法求逆元
当且仅当gcd(a, p)=1的时候该方程有解 a * x - p * y = 1;
void gcd(int a, int b, int &x,int &y, int &d) { if (b) { int t = x; gcd(b, a % b, x, y, d); x = y; y = t - a / b * y; else { d = a, x = 1, y = 0; } }//主要目的是最后的x int inv(int t, int p)//如果都不存在的话返回-1 { int d, x, y; gcd(t, p, x, y, d); return d == 1 ? (x % p + p) % p : -1; }//求t的逆元(对p取模)
基本数据结构
链表
单链表
一般为邻接表,用于存储图或者树
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点 int head, e[N], ne[N], idx; // 初始化 void init() { head = -1; idx = 0; } // 在链表头插入一个数a void insert(int a) { e[idx] = a, ne[idx] = head, head = idx ++ ; } // 将头结点删除,需要保证头结点存在 void remove() { head = ne[head]; } // 将x插入到下标是k的点后面 void add(int k, int x) { e[idx] = x; ne[idx] = ne[k]; ne[k] = idx++; } // 将下标是k的点后面的点删除 void remove(int k) { ne[k] = ne[ne[k]]; }
双链表
一般用于优化某些算法
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点 int e[N], l[N], r[N], idx; // 初始化 void init() { //0是左端点,1是右端点 r[0] = 1, l[1] = 0; idx = 2; } // 在节点a的右边插入一个数x void insert(int a, int x) { e[idx] = x; l[idx] = a, r[idx] = r[a]; l[r[a]] = idx, r[a] = idx ++ ; } // 删除节点a void remove(int a) { l[r[a]] = l[a]; r[l[a]] = r[a]; }
队列
普通队列
// hh 表示队头,tt表示队尾 int q[N], hh = 0, tt = -1; // 向队尾插入一个数 q[ ++ tt] = x; // 从队头弹出一个数 hh ++ ; // 队头的值 q[hh]; // 判断队列是否为空 if (hh <= tt) { }
循环队列
// hh 表示队头,tt表示队尾的后一个位置 int q[N], hh = 0, tt = 0; // 向队尾插入一个数 q[tt ++ ] = x; if (tt == N) tt = 0; // 从队头弹出一个数 hh ++ ; if (hh == N) hh = 0; // 队头的值 q[hh]; // 判断队列是否为空 if (hh != tt) {}
单调队列
常见模型:找出滑动窗口中的最大值/最小值 int hh = 0, tt = -1; for (int i = 0; i < n; i ++ ) { while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口 while (hh <= tt && check(q[tt], i)) tt -- ; q[ ++ tt] = i; }
栈
普通栈
// tt表示栈顶 int stk[N], tt = 0; // 向栈顶插入一个数 stk[ ++ tt] = x; // 从栈顶弹出一个数 tt -- ; // 栈顶的值 stk[tt]; // 判断栈是否为空 if (tt > 0) { }
单调栈
基本思路就是先按照暴力办法想一下,再思考其中是否有什么隐藏的规律,再根据那个规律来写
//常见模型:找出每个数左边离它最近的比它大/小的数 int tt = 0; for (int i = 1; i <= n; i ++ ) { while (tt && check(stk[tt], i)) tt -- ; stk[ ++ tt] = i; }
KMP
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度 求模式串的Next数组: for (int i = 2, j = 0; i <= m; i ++ ) { while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j; } // 匹配 for (int i = 1, j = 0; i <= n; i ++ ) { while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == m) { j = ne[j]; // 匹配成功后的逻辑 } }
Trie树
int son[N][26], cnt[N], idx; // 0号点既是根节点,又是空节点 // son[][]存储树中每个节点的子节点 // cnt[]存储以每个节点结尾的单词数量 // 插入一个字符串 void insert(char *str) { int p = 0; for (int i = 0; str[i]; i ++ ) { int u = str[i] - 'a'; if (!son[p][u]) son[p][u] = ++ idx; p = son[p][u]; } cnt[p] ++ ; } // 查询字符串出现的次数 int query(char *str) { int p = 0; for (int i = 0; str[i]; i ++ ) { int u = str[i] - 'a'; if (!son[p][u]) return 0; p = son[p][u]; } return cnt[p]; }
并查集
朴素并查集
int p[N]; //存储每个点的祖宗节点 // 返回x的祖宗节点 int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } // 初始化,假定节点编号是1~n for (int i = 1; i <= n; i ++ ) p[i] = i; // 合并a和b所在的两个集合: p[find(a)] = find(b);
维护size的并查集
int p[N], size[N]; //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量 // 返回x的祖宗节点 int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } // 初始化,假定节点编号是1~n for (int i = 1; i <= n; i ++ ) { p[i] = i; size[i] = 1; } // 合并a和b所在的两个集合: size[find(b)] += size[find(a)]; p[find(a)] = find(b);
维护到祖宗节点距离的并查集
int p[N], d[N]; //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离 // 返回x的祖宗节点 int find(int x) { if (p[x] != x) { int u = find(p[x]); d[x] += d[p[x]]; p[x] = u; } return p[x]; } // 初始化,假定节点编号是1~n for (int i = 1; i <= n; i ++ ) { p[i] = i; d[i] = 0; } // 合并a和b所在的两个集合: p[find(a)] = find(b); d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
堆
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1 // ph[k]存储第k个插入的点在堆中的位置 // hp[k]存储堆中下标是k的点是第几个插入的 int h[N], ph[N], hp[N], size; // 交换两个点,及其映射关系 void heap_swap(int a, int b) { swap(ph[hp[a]],ph[hp[b]]); swap(hp[a], hp[b]); swap(h[a], h[b]); } void down(int u) { int t = u; if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2; if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; if (u != t) { heap_swap(u, t); down(t); } } void up(int u) { while (u / 2 && h[u] < h[u / 2]) { heap_swap(u, u / 2); u >>= 1; } } // O(n)建堆 for (int i = n / 2; i; i -- ) down(i);
哈希表
一般哈希
拉链法
int h[N], e[N], ne[N], idx; // 向哈希表中插入一个数 void insert(int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++ ; } // 在哈希表中查询某个数是否存在 bool find(int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1; i = ne[i]) if (e[i] == x) return true; return false; }
开放寻址法
int h[N]; // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置 int find(int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0; } return t; }
字符串哈希
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低 小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果 typedef unsigned long long ULL; ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64 // 初始化 p[0] = 1; for (int i = 1; i <= n; i ++ ) { h[i] = h[i - 1] * P + str[i]; p[i] = p[i - 1] * P; } // 计算子串 str[l ~ r] 的哈希值 ULL get(int l, int r) { return h[r] - h[l - 1] * p[r - l + 1]; }
C++ STL简介
vector
变长数组,倍增的思想 size() 返回元素个数 empty() 返回是否为空 clear() 清空 front()/back() push_back()/pop_back() begin()/end() [] 支持比较运算,按字典序
pair<int, int>
first, 第一个元素 second, 第二个元素 支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string
字符串 size()/length() 返回字符串长度 empty() clear() substr(起始下标,(子串长度)) 返回子串 c_str() 返回字符串所在字符数组的起始地址
queue
队列 size() empty() push() 向队尾插入一个元素 front() 返回队头元素 back() 返回队尾元素 pop() 弹出队头元素
priority_queue
优先队列,默认是大根堆 size() empty() push() 插入一个元素 top() 返回堆顶元素 pop() 弹出堆顶元素 定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack
栈 size() empty() push() 向栈顶插入一个元素 top() 返回栈顶元素 pop() 弹出栈顶元素
deque
双端队列 size() empty() clear() front()/back() push_back()/pop_back() push_front()/pop_front() begin()/end() []
动态维护有序序列
set, map, multiset, multimap基于平衡二叉树(红黑树),动态维护有序序列 size() empty() clear() begin()/end() ++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数 find() 查找一个数 count() 返回某一个数的个数 erase() (1) 输入是一个数x,删除所有x O(k + logn) (2) 输入一个迭代器,删除这个迭代器 lower_bound()/upper_bound() lower_bound(x) 返回大于等于x的最小的数的迭代器 upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair erase() 输入的参数是pair或者迭代器 find() [] 注意multimap不支持此操作。 时间复杂度是 O(logn) lower_bound()/upper_bound()
哈希表
unordered_set, unordered_map, unordered_multiset, unordered_multimap,哈希表和上面类似,增删改查的时间复杂度是 O(1) 不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset, 圧位 bitset<10000> s; ~, &, |, ^ >>, << ==, != []
count() 返回有多少个1
any() 判断是否至少有一个1 none() 判断是否全为0
set() 把所有位置成1 set(k, v) 将第k位变成v reset() 把所有位变成0 flip() 等价于~ flip(k) 把第k位取反
图的存储与遍历
(这一部分并不是所提供的标准代码,所以可能存在一部分问题,等待后期修正
存储
无向图是特殊的有向图,树是特殊的图,所以这部分的话主要掌握有向图的写法即可
const int N = 100010; int h[N], e[N], ne[N], idx; //h存放头节点,e存放结点的值(val,ne存放结点的下一个(空指针就用-1来代替,d记录路程, idx存放的是e和ne用到哪里了 void add(int a, int b)//存储a到b的路 { e[idx] = b; ne[idx] = h[a]; h[a] = idx++; } //同时注意初始化问题 memset(h, -1, sizeof h);
以上是针对于权值相等的情况,如果边的权值各不相同的话需要引用下一个模板
const int N = 100010, M = N * 2; int w[N], h[N], e[M], ne[M], idx; void add(int a, int b, int c)//存储a到b的路 { w[idx] = c; e[idx] = b; ne[idx] = h[a]; h[a] = idx++; }
其实相较于上一个写法,只是多了一个数组来存储边的权值
遍历
for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (!d[j]) {/* ... */} }//基本上面if之前的代码是相同的
//可以参考下面遍历中 DFS 和 BFS 关于邻接表的写法
搜索
DFS
用的基本数据结构为栈,思路为,每检索到一个点就将其入栈,然后继续向下检索(持续进行这一操作,等到已无数据可以入栈的时候,开始返回,每返回碰到一个栈里面的数据就将其出栈,然后继续检索,重复以上操作,直到栈内已经没有元素可以用来检索的时候返回
模板:
int dfs(int u) { st[u] = true; // st[u] 表示点u已经被遍历过 for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) dfs(j); } }
//
void Dfs(int u)//U实际上是一个计数的作用,算到递归到哪一层就可以停止递归操作 { int k; if (u == m) { for (k = 0; k < m; k++) printf("%d ", s[k]); putchar('\n'); //将求得的数组输出 return ; } //递归部分操作 for (k = s[u - 1] + 1; k <= n; k++) { s[u] = k; Dfs(u + 1); } }
BFS
实现bfs算法的基本数据结构为队列,基本思想为,检索每一层的每一种可能性并将其依次入队,然后继续检索队列的第一种情况然后将其出队,将队列第一种情况能够衍生的情况全部检索并依次入队,重复以上操作,直到所有情况检索完毕或者已经检索到目标情况(一般是后者的时候停止检索。
模板:
queue<int> q; st[1] = true; // 表示1号点已经被遍历过 q.push(1); while (q.size()) { int t = q.front(); q.pop(); for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) { st[j] = true; // 表示点j已经被遍历过 q.push(j); } } }
//
queue<PII> q; q.push({mx[0], my[0]}); while (q.size()) { int xx = q.front().first, yy = q.front().second; q.pop(); int dx[8] = {-1, -2, 1, 2, -1, -2, 1, 2}, dy[8] = {2, 1, 2, 1, -2, -1, -2, -1};//这个题目的运动规则较为特殊 for (int k = 0; k < 8; k++) { int x = xx + dx[k], y = yy + dy[k]; if (x >= 0 && y >= 0 && x < c && y < r && !d[x][y] && m[x][y] != '*') //这条路线是可行的话入队 { q.push({x, y}); d[x][y] = d[xx][yy] + 1; m[x][y] = '*';//充当了标记的作用 } } } return d[mx[1]][my[1]]; //mx和my数组存的是起始点的位置
一般而言可以再开一个bool数组来存储点的遍历情况,但是对迷宫类型的问题的话可以选择直接更新地图(根据始积需要来,并不一定是这样)
分析总结:
这两种都用于遍历搜索题型,dfs的典型为n皇后问题,bfs的话为迷宫系列问题。(如果想要重温算法的话可以找这两种类型的题目来快速熟悉
配合实践所用的数据结构分别为栈和队列
上面两种分别是针对邻接表和邻接数组的(同时邻接数组yxc并没有给出参考模板,所以是根据所做的题目自己写的模板,可能会有问题
个人使用较多的是dfs,实际应用中遇到最多的问题是初始化,最大值最好还是写成0x3f3f3f而不是随便写一个值(为什么不写0x7f7f7f则是因为有需要相加的情况下会越界导致答案错误)
最短路算法
单源最短路
无负权边
稠密图:
Dijkstra-朴素O(n^2)
初始化距离数组, dist[1] = 0, dist[i] = inf; for n次循环 每次循环确定一个min加入S集合中,n次之后就得出所有的最短距离 将不在S中dist_min的点->t t->S加入最短路集合 用t更新到其他点的距离
模板:
int g[N][N]; // 存储每条边 int dist[N]; // 存储1号点到每个点的最短距离 bool st[N]; // 存储每个点的最短路是否已经确定 // 求1号点到n号点的最短路,如果不存在则返回-1 int dijkstra() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; for (int i = 0; i < n - 1; i ++ ) { int t = -1; // 在还未确定最短路的点中,寻找距离最小的点 for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; // 用t更新其他点的距离 for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], dist[t] + g[t][j]); st[t] = true; } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; }
稀疏图:
Dijkstra-堆优化O(mlogm)
利用邻接表,优先队列 在priority_queue[HTML_REMOVED], greater[HTML_REMOVED] > heap;中将返回堆顶 利用堆顶来更新其他点,并加入堆中类似宽搜
模板:
typedef pair<int, int> PII; int n; // 点的数量 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 int dist[N]; // 存储所有点到1号点的距离 bool st[N]; // 存储每个点的最短距离是否已确定 // 求1号点到n号点的最短距离,如果不存在,则返回-1 int dijkstra() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push({0, 1}); // first存储距离,second存储节点编号 while (heap.size()) { auto t = heap.top(); heap.pop(); int ver = t.second, distance = t.first; if (st[ver]) continue; st[ver] = true; for (int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > distance + w[i]) { dist[j] = distance + w[i]; heap.push({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; }
关于这部分堆优化的实现,原理而言可以手写堆来实现,但是就算法本身而言,优先队列已经可以满足需求
有负权边
有负权回路:
Bellman_ford O(nm)
算法原理: 其实就是对每个点都做全部边的松弛操作,原理上很容易想,但是存在的问题就是时间复杂度过高
注意连锁想象需要备份, struct Edge{inta,b,c} Edge[M]; 初始化dist, 松弛dist[x.b] = min(dist[x.b], backup[x.a]+x.w); 松弛k次,每次访问m条边
模板:
int n, m; // n表示点数,m表示边数 int dist[N]; // dist[x]存储1到x的最短路距离 int back[N]; //备份数组防止串联 struct Edge // 边,a表示出点,b表示入点,w表示边的权重 { int a, b, w; }edges[M]; // 求1到n的最短路距离,如果无法从1走到n,则返回-1。 int bellman_ford() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。 for (int i = 0; i < n; i ++ ) { memcpy(back, dist, sizeof dist); for (int j = 0; j < m; j ++ ) { int a = edges[j].a, b = edges[j].b, w = edges[j].w; if (dist[b] > dist[a] + w) dist[b] = dist[a] + w; } } if (dist[n] > 0x3f3f3f3f / 2) return -1; return dist[n]; }
无负权回路:
spfa算法的原理: 本质上是对bellman_ford算法的优化,为了降低时间复杂度利用队列优化仅加入修改过的地方 for k次 for 所有边利用宽搜模型去优化bellman_ford算法 更新队列中当前点的所有出边
//更深一层的优化:用st存储数据修改信息,如果数据已经在队列里面,那么不需要将它重复入队(但是不写st数组并不影响总体代码的实现)
int n; // 总点数 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 int dist[N]; // 存储每个点到1号点的最短距离 bool st[N]; // 存储每个点是否在队列中 // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1 int spfa() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; queue<int> q; q.push(1); st[1] = true; while (q.size()) { auto t = q.front(); q.pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入 { q.push(j); st[j] = true; } } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; }
关于spfa算法,它本质上是BF算法的优化,效率取决于题目所给数据,最差的情况下可以被卡成n^2(一半来说网状图容易卡数据),同时spfa算法的适用性很广,一般情况下可以取代迪杰斯特拉算法
多源最短路
Floyd O(n^3)
初始化d k, i, j 去更新d
初始化: for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) if (i == j) d[i][j] = 0; else d[i][j] = INF; // 算法结束后,d[a][b]表示a到b的最短距离 void floyd() { for (int k = 1; k <= n; k ++ ) for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }
简单但是容易超时,数据不大的时候可以直接使用,但是如果数据过大的话最好选择其他算法或者根据题目需要对算法进行改进
拓扑排序
bool topsort() { int hh = 0, tt = -1; // d[i] 存储点i的入度 for (int i = 1; i <= n; i ++ ) if (!d[i]) q[ ++ tt] = i; while (hh <= tt) { int t = q[hh ++ ]; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (-- d[j] == 0) q[ ++ tt] = j; } } // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。 return tt == n - 1; }
spfa判断图中是否存在负环
int n; // 总点数 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数 bool st[N]; // 存储每个点是否在队列中 // 如果存在负环,则返回true,否则返回false。 bool spfa() { // 不需要初始化dist数组 // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。 queue<int> q; for (int i = 1; i <= n; i ++ ) { q.push(i); st[i] = true; } while (q.size()) { auto t = q.front(); q.pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; cnt[j] = cnt[t] + 1; if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环 if (!st[j]) { q.push(j); st[j] = true; } } } } return false; }
求最小生成树
朴素版prim算法
int n; // n表示点数 int g[N][N]; // 邻接矩阵,存储所有边 int dist[N]; // 存储其他点到当前最小生成树的距离 bool st[N]; // 存储每个点是否已经在生成树中 // 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和 int prim() { memset(dist, 0x3f, sizeof dist); int res = 0; for (int i = 0; i < n; i ++ ) { int t = -1; for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; if (i && dist[t] == INF) return INF; if (i) res += dist[t]; st[t] = true; for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]); } return res; }
Kruskal算法
int n, m; // n是点数,m是边数 int p[N]; // 并查集的父节点数组 struct Edge // 存储边 { int a, b, w; bool operator< (const Edge &W)const { return w < W.w; } }edges[M]; int find(int x) // 并查集核心操作 { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int kruskal() { sort(edges, edges + m); for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集 int res = 0, cnt = 0; for (int i = 0; i < m; i ++ ) { int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find(a), b = find(b); if (a != b) // 如果两个连通块不连通,则将这两个连通块合并 { p[a] = b; res += w; cnt ++ ; } } if (cnt < n - 1) return INF; return res; }