开个新坑,距离数据结构期末考试 2023/12/26 还有 1 天
这学期似乎什么题都做不到,随着我期末复习在这里把一些主要算法理一下
主要涵盖数据结构大部分知识点,语言为C++。
主要内容是整理一些算法的 模板题 和 整体思路 / 注意点
来源应该是AcWing的算法基础课和学校老师的PPT
之后的更新我就直接编辑这篇文章了。
快速排序
先上模板:
void quickSort(vector<int>& arr, int l, int r){
if(l>=r) return;
int i = l-1, j = r+1, x = arr[(l+r)>>1];
while(i<j){
do ++i; while(arr[i]<x);
do --j; while(arr[j]>x);
if(i<j) swap(arr+i, arr+j);
}
quickSort(arr, l, j);
quickSort(arr,j+1,r);
}
思路:
- 随机选一个 x (这里是中间点的值),左右指针遍历数组,使得
>=x
的数都在 x 后,反之亦然。 - 然后递归完成后续操作。
- 这里 swap 附近循环可以巧妙地原地操作,而不需要额外的空间。
- 边界条件 i,j,l,r 太多太杂,直接背模板是最稳妥的。
相关题目
第k个数
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
vector<int> arr(N);
int n, k;
int kth_sort(int k, int l , int r){
if(l==r) return arr[l];
int i = l-1, j=r+1, x = arr[(l+r)>>1];
while(i<j){
do ++i; while(arr[i]<x);
do --j; while(arr[j]>x);
if(i<j) swap(arr[i], arr[j]);
}
int dif = j-l+1;
if(dif>=k) return kth_sort(k, l,j);
return kth_sort(k-dif,j+1,r);
}
int main(){
cin>>n>>k;
for(int& t:arr) cin>>t;
int p = kth_sort(k,0,n-1);
cout<<p;
}
要点:
- 注意递归终止条件
————以上写于2023年12月19日零点前后,醒来写归并(或许吧)————
归并排序
模板:
void merge_sort(vector<int>& arr, int l , int r){
if(l>=r) return ;
int m = (l+r)>>1;
merge_sort(arr,l,m),
merge_sort(arr,m+1,r);
int i=l, j=m+1,k=0;
while(i<=m && j<=r){
if(arr[i]<=arr[j]) tmp[k++]=arr[i++];
else tmp[k++]=arr[j++];
}
while(i<=m) tmp[k++]=arr[i++];
while(j<=r) tmp[k++]=arr[j++];
for(int i=l,j=0;i<=r;++i,++j) arr[i]= tmp[j];
}
思路:
- 要点是如何合并子区间
- 同样要注意左右指针以及分段后的处理范围
相关题目:
逆序对的个数
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1e6+5;
vector<int> arr;
int n;
unsigned long u=0;
void merge_sort(vector<int>& arr, int l , int r){
if(l>=r) return;
int m = (l+r)>>1;
merge_sort(arr,l,m),
merge_sort(arr,m+1,r);
int i=l, j=m+1,k=0;
vector<int> tmp(r-l+1);
while(i<=m && j<=r){
if(arr[i]<=arr[j]) tmp[k++]=arr[i++];
else {
u += m -i + 1;
tmp[k++]=arr[j++];
}
}
while(i<=m) tmp[k++]=arr[i++];
while(j<=r) tmp[k++]=arr[j++];
for(int i=l,j=0;i<=r;++i,++j) arr[i]= tmp[j];
}
int main(){
cin>>n;
arr.resize(n);
for(int i=0;i<n;++i)
{
cin>>arr[i];
}
merge_sort(arr,0,n-1);
cout<<u<<" ";
}
思路:
- 只需要在模板中加入 什么时候逆序对数量会增多 以及 增加多少 的判断即可。
- 如果左半部分
arr[i]
大于右半部分的arr[j]
,那么由于左和右都已排序好,所以从arr[i]
到arr[m]
的所有元素都会大于arr[j]
。因此,与arr[j]
构成的逆序对的数量就是m - i + 1
。
————以上写于2023年12月19日上午,下午写二分前缀和(或许吧)————
————又到凌晨咯————
前缀和
实用的小妙招,用于当你需要O(N)求和,可达到满N立减100%的妙用。
理解起来也是很方便的,直接先看模板题的代码:
求前缀和
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
vector<int> arr(N);
vector<int> sum(N);
int n,m;
int main(){
ios::sync_with_stdio(false); cin.tie(0);
cin>>n>>m;
sum[0]=0;
for(int i=1;i<=n;++i) {
cin>>arr[i];
sum[i] = sum[i-1]+arr[i];
}
while(m--){
int l,r; cin>>l>>r;
cout<<sum[r]-sum[l-1]<<endl;
}
}
————以上写于2023年12月20日凌晨,困比了,起来写二分————
二维前缀和
- 没什么难点,在矩阵里画一下加减的情况就可以把下标问题搞定。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
vector<vector<int>> arr(N,vector<int>(N));
vector<vector<int>> sum(N,vector<int>(N));
int n,m,q;
int main(){
ios::sync_with_stdio(false); cin.tie(0);
cin>>n>>q>>m;
for(int j=0;j<=n;++j){
for(int i=0;i<=q;++i) {
if(i==0 ||j==0) arr[j][i] =0;
else{
cin>>arr[j][i];
sum[j][i] = arr[j][i]+sum[j-1][i]+sum[j][i-1]-sum[j-1][i-1];
}
}
}
while(m--){
int l1,r1,l2,r2; cin>>l1>>r1>>l2>>r2;
cout<<sum[l2][r2]+sum[l1-1][r1-1]-sum[l1-1][r2]-sum[l2][r1-1]<<endl;
}
return 0;
}
差分
- 可以理解成前缀和的逆运算,主要核心部分我附在注释里
差分模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n,m;
int l,r,c;
vector<int> arr(N), b(N);
void insert(int l,int r, int a){
b[l] +=a;
b[r+1] -=a;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;++i) {
cin>>arr[i];
insert(i,i,arr[i]);
/*这一步是关键,录入arr[i],相当于在
[i,i]区间内插入一个arr[i].
因此差分中的所有操作都可以转化为insert操作*/
}
while(m--){
cin>>l>>r>>c;
insert(l,r,c);
}
for(int i=1;i<=n;++i){
arr[i] = arr[i-1] +b[i];
}
for(int i=1;i<=n;++i) cout<<arr[i]<<" ";
}
差分矩阵
只要理解了二维前缀和 & 差分,这个问题就不难解决。
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e3+10;
vector<vector<int>> arr(N,vector<int>(N)),b(N,vector<int>(N));
int n,m,q;
void insert(int l1,int r1,int l2,int r2, int a){
b[l1][r1] +=a;
b[l1][r2+1] -=a;
b[l2+1][r1] -=a;
b[l2+1][r2+1] +=a;
}
int main(){
cin>>n>>m>>q;
for(int i=0;i<=n;++i){
for(int j=0;j<=m;++j){
if(i==0 || j==0) arr[i][j]=0;
else {
cin>>arr[i][j];
insert(i,j,i,j,arr[i][j]);
}
}
}
while(q--){
int j,k,l,q,w;
cin>>j>>k>>l>>q>>w;
insert(j,k,l,q,w);
}
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
arr[i][j] = arr[i-1][j] +arr[i][j-1] -arr[i-1][j-1] +b[i][j];
cout<<arr[i][j]<<" ";
}
cout<<endl;
}
return 0;
}
双指针
思路:
- 双指针是对朴素暴力
O(N^2)
的优化,因为在O(N^2)
中发现有些遍历其实是重复的,双指针是在遍历i
的基础上,有条件地移动j
,使得一共遍历的次数≤ 2*n
次,达到O(N)
的复杂度。
数的范围
乏善可陈,用到了哈希表优化
#include<iostream>
#include<vector>
#include<unordered_set>
using namespace std;
const int N = 1e5+5;
vector<int> a(N);
unordered_set<int> calc;
int n;
int main(){
cin>>n;
int res =0;
for(int i=0;i<n;++i) cin>>a[i];
for(int i=0,j=0;i<n;++i){
while(calc.count(a[i])>0){
calc.erase(a[j]);
++j;
}
if(j<=i) calc.insert(a[i]);
res = max(res,i-j+1);
}
cout<<res;
}
求出满足 A[i]+B[j]=x
的数对 (i,j)
思路:
- 很好的一道有助于理解双指针核心思想的题目。
- a数组从前往后遍历,b数组从后往前遍历,根据单调性,对于每一个
i
, 先找到满足a[i]+b[j]<=x
的最大的 j 。 - 如果此时等式不成立,那么a继续往后,而此时因为a变大了,所以b一定不会变大。
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e5+5;
vector<int> a(N),b(N);
int n,m,x,l;
int main(){
ios::sync_with_stdio(false); cin.tie(0);
cin>>n>>m>>x;
for(int i=0;i<n;++i) cin>>a[i];
for(int j=0;j<m;++j) cin>>b[j];
l=m-1;
for(int i=0;i<n;++i){
while(a[i]+b[l]>x && l>=0) --l;
if(a[i]+b[l]==x ){
cout<<i<<" "<<l;
return 0;
}
else continue;
}
}
判断 a 数组是否为 b 数组的子序列
很巧妙的一道题,看过就很难忘记。
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e5+5;
vector<int> a(N),b(N);
int n,m,j,i;
int main(){
cin>>n>>m;
for(int i=0;i<n;++i) cin>>a[i];
for(int i=0;i<m;++i) cin>>b[i];
j=0, i=0;
while(i<n &&j<m){
if(b[j]==a[i]) ++i;
++j;
}
if(i<n) puts("No");
else puts("Yes");
return 0;
}
链表
用数组实现,关键在于对idx的理解。
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e5+5;
vector<int> a(N), ne(N);
int head , idx,x;
void insert_head(int x){
a[idx] =x;
ne[idx]=head;
head = idx++;
}
void add(int k,int x){
a[idx]=x;
ne[idx]=ne[k];
ne[k]= idx++;
}
void del(int k){
ne[k] =ne[ne[k]];
}
int main(){
idx=0;
head=-1;
int m;cin>>m;
while(m--){
char c; cin>>c;
int k;
switch(c){
case 'H': cin>>x; insert_head(x); break;
case 'D':
cin>>k;
if(!k) head = ne[head];
else del(k-1);
break;
case 'I':
cin>>k>>x;
add(k-1,x);
break;
}
}
for(int i=head;i !=-1;i=ne[i]) cout<<a[i]<<" ";
}
栈
表达式求值
- 两个栈分别存运算符和数,注意出入栈时机。
#include<iostream>
#include<vector>
#include<stack>
#include <algorithm>
#include<unordered_map>
using namespace std;
const int N = 1e5+5;
stack<int> num;
stack<char> op;
int i,j;
char c;
void calc(){
int i1 = num.top(); num.pop();
int i2 = num.top(); num.pop();
char c = op.top();op.pop();
if(c=='+') num.push(i1 + i2);
else if(c=='-') num.push(-i1 + i2);
else if(c=='*') num.push(i1 * i2);
else if(c=='/') num.push(i2/i1);
}
int main(){
unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
string s;
cin>>s;
for(int i=0;i<s.size();++i){
auto c=s[i];
if (isdigit(c))
{
int x = 0, j = i;
while (j < s.size() && isdigit(s[j]))
x = x * 10 + s[j ++ ] - '0';
i = j - 1;
num.push(x);
}
else if(c=='(') op.push(c);
else if(c==')'){
while(op.top() != '(') calc();
op.pop();
}
else{
while(op.size() &&op.top() != '(' && pr[op.top()]>=pr[c]) calc();
op.push(c);
}
}
while(op.size()) calc();
cout<<num.top();
}
单调栈 - 输出每个数左边第一个比它小的数
- 又说题型只有一种,就是找一列数中某个数最近的比它大/小的数,或许是这样。
- 解题方法:
- 先像朴素做法,然后考虑重复的步骤可以如何减少,什么时候如满足
A
,则一定不会发生B
,然后在数据结构中实现。 - 对于这道题,当发现
a[i-1] > a[i]
,则a[i+1]
要找到的满足条件的数一定不会是a[i]
。
- 先像朴素做法,然后考虑重复的步骤可以如何减少,什么时候如满足
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
vector<int> a(N);
stack<int> small;
int m;
int main(){
cin>>m;
for(int i=0;i<m;++i) {
cin>>a[i];
while(!small.empty() && small.top()>=a[i]) small.pop();
if(small.empty()) cout<<-1<<" ";
else cout<<small.top()<<" ";
small.push(a[i]);
}
}
滑动窗口(单调队列/双指针)
输入格式
输入包含两行。
第一行包含两个整数 n和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
#include<iostream>
using namespace std;
const int N = 1e6+10;
int n,m,a[N],q[N];
int main(){
cin>>n>>m;
int h1=0,h2=0, t1=-1,t2=-1;
for(int i=1;i<=n;++i){
cin>>a[i];
if(h1<=t1 && i-q[h1]+1>m) ++h1;
while(h1<=t1 && a[q[t1]]>=a[i]) --t1;
q[++t1] = i;
if(i>m-1) cout<<a[q[h1]]<<" ";
}
cout<<endl;
for(int i=1;i<=n;++i){
if(h2<=t2 && i-q[h2]+1>m) ++h2;
while(h2<=t2 && a[q[t2]]<=a[i]) --t2;
q[++t2] = i;
if(i>m-1) cout<<a[q[h2]]<<" ";
}
}
KMP
- 难点在于求next数组
- 事实上不管是main函数的查询还是next数组的求解都是双指针问题
- 对于main函数,终止条件是要查询的数组,有索引
i==n
。 - 对于next数组的求解,本质也是当前索引和前缀的双指针。
模板
#include<iostream>
using namespace std;
const int N = 1e6+10;
int n,m,ne[N];
char s[N],p[N];
int main(){
cin>>n>>p+1>>m>>s+1;
for(int i=2,j=0;i<=n;++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<=m;++i){
while(j && s[i]!= p[j+1] ) j=ne[j];
if(s[i] ==p[j+1]) ++j;
if(j==n){
cout<<i-n<<" ";
j=ne[j];
}
}
}
Trie 树
- 用来解决一些类似字典/单词索引的问题
- 其实有一些链表的思想
- 也可以看作用树的邻接表来存储单词等
模板(对单词的insert/query)
#include<iostream>
#include<vector>
#include<string>
using namespace std;
const int N = 1e5+5;
int a[N][26];
int cnt[N],n,idx=0;
string s;
void insert(string s){
int p=0; //root
for(char c:s){
int u = c-'a';
if(!a[p][u]) a[p][u] = ++idx;
p = a[p][u];
}
cnt[p] ++; //mark
}
int query(string s){
int p=0;
for(char c:s){
int u = c-'a';
if(!a[p][u]) return 0;
p=a[p][u];
}
return cnt[p];
}
int main(){
ios::sync_with_stdio(false); cin.tie(0);
cin>>n;
while(n--){
char c; cin>>c>>s;
if(c=='I'){
insert(s);
}
else{
int i = query(s);
cout<<i<<endl;
}
}
}
最大异或对
- 好难的题
- 用Trie树存储每个数的二进制表示,每次寻找最大异或值时,使走上与该数不同的分支,并从最高位开始,可使异或值最大
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5+5, M= 3100010;
int m[M][2], n,idx,a[N];
void insert(int k){
int p=0;
for(int i=30;i>=0;--i){
int u = k>>i&1;
if(!m[p][u]) m[p][u] = ++idx;
p = m[p][u];
}
}
int query(int k){
int p=0;
int res=0;
for(int i=30;i>=0;--i){
int u = k>>i&1;
if(m[p][!u]) {
p=m[p][!u];
res += 1 << i;
}
else{
p= m[p][u];
}
}
return res;
}
int main(){
int q,r,res =0;
cin>>n;
for(int i=0;i<n;++i){
cin>>a[i];
insert(a[i]);
}
for (int i = 0; i < n; i ++ ) {
res = max(res, query(a[i]));
}
cout<<res;
}
并查集
- 用近乎 O(1) 的复杂度实现两个集合的合并、查找。
- 最关键的代码是
if(a!=f[a]) a = find(f[a])
在查找祖先的路上也实现了路径压缩。
#include<iostream>
using namespace std;
const int N = 1e5+5;
int f[N],n,m;
int find(int a){
if(f[a]!=a) f[a]=find(f[a]);
return a;
}
void insert(int a,int b){
a=find(a); b=find(b);
f[find(a)] = find(b);
}
void judge(int a,int b){
a=find(a);b=find(b);
if(a==b) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
int main(){
cin>>n>>m;
for(int i=1;i<= n;++i) f[i]=i;
char c;
int a,b;
while(m--){
cin>>c>>a>>b;
if(c=='M'){
insert(a,b);
}
else{
judge(a,b);
}
}
}
堆
O(logn)
复杂度实现插入/删除(up/down)O(1)
复杂度给出最值
用数组实现堆的给出最值/删除
思路:
- 对于数组中处在位置
x
的元素,它的左右儿子分别在2x
&2x+1
down
操作中,比较2x
&2x+1
和x
的大小,如果小,就交换- 对于down操作从哪里开始 由于最大元素是
n
,所以从n/2+1
到n
的元素都位于堆的最后一排,因此不需要down
了.
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e6+5;
int n,m;
int h[N], cnt,i;
void down(int u){
int t =u;
if(2*u<=cnt && h[2*u]<h[t]) t=2*u;
if(2*u+1<=cnt && h[2*u+1]<h[t]) t=2*u+1;
if(u!=t){
swap(h[u],h[t]);
down(t);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>h[i];
cnt=n;
for(int i=n/2;i;--i) down(i);
while(m--){
cout<<h[1]<<" ";
h[1] = h[cnt--];
down(1);
}
puts(" ");
}
哈希/散列表
模拟散列表
#include<iostream>
#include<cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
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;
}
int main(){
memset(h,0x3f, sizeof h);
int n;
cin>>n;
while(n--){
char c;
int x;
cin>>c>>x;
if(c=='I') h[find(x)] =x;
else{
if(h[find(x)] ==null) puts("No");
else puts("Yes");
}
}
}
字符串哈希
核心:通过base
将字符串转换成唯一的数与之对应
注意边界条件
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int base = 131, M = 1e5+5;
int n,m ;
unsigned long long a[M],p[M];
string s;
int main(){
cin>>n>>m;
cin>>s;
p[0]=1;
int e,f,g,h;
for(int i=1;i<=n;++i){
a[i] = a[i-1]*base + s[i-1]-'0';
p[i]=p[i-1]*base;
}
while(m--){
cin>>e>>f>>g>>h;
if((a[f]-a[e-1]*p[f-e+1]) == (a[h]-a[g-1]*p[h-g+1])) puts("Yes");
else puts("No");
}
}
DFS - 深度优先搜索
- 当前位置、回溯、剪枝
1~n的全排列
#include <iostream>
using namespace std;
const int N = 10;
int post[N],n,m,u=0;
bool st[N]={false};
void dfs(int u){ // u - 当前位置
if(u==n) {
for(int i=0;i<n;++i) cout<<post[i]<<" "; /* 到达底层,完成一条路*/
puts(" ");
return;
}
for(int i=1;i<=n;++i){
if(!st[i]){
post[u] = i;
st[i] = true;
dfs(u+1);
st[i] = false; // 恢复现场
}
}
}
int main(){
cin>>n;
dfs(0);
return 0;
}
N皇后问题
- 剪枝的思想:在每次递归寻找可行解的时候加一些特判条件,减小时间复杂度
#include<iostream>
using namespace std;
const int N = 20;
char pic[N][N];
int n;
bool col[N]={false},dig[N*2]={false},xig[N*2]={false};
void dfs(int u){
if(u==n){
for(int i=0;i<n;++i) puts(pic[i]);
cout<<endl;
return;
}
for(int i=0;i<n;++i){
if(!col[i]&&!dig[u+i] &&!xig[n-u+i]){
pic[u][i] = 'Q';
col[i]=true; dig[u+i]=true; xig[n-u+i]=true; //截距、剪枝
dfs(u+1);
col[i]=false; dig[u+i]=false; xig[n-u+i]=false;
pic[u][i]='.'; //注意到这五行是对称的,每次得到一个可行解后恢复原状
}
}
}
int main(){
cin>>n;
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
pic[i][j] = '.';
}
}
dfs(0);
return 0;
}
BFS
- 具体思路和细节标注在了代码中
01迷宫
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
queue< pair<int,int> >q;
typedef pair<int, int>PII;
int n,m;
int g[N][N],d[N][N]; //g存图,d存距离(路程)
int bfs(){
queue<pair<int, int> >q; //q存位置点(坐标)
q.push({0,0});
memset(d,-1,sizeof(d));
d[0][0]=0;
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1}; //四个方向,便于遍历
while(q.size()){
PII t = q.front();
q.pop();
for(int i=0;i<4;++i){ //四种方向均遍历到,当存在一种可行解,结束
int x = t.first +dx[i];
int y = t.second +dy[i];
if(x>=0 &&x<n &&y>=0 && y<m && g[x][y]==0 && d[x][y] == -1){
d[x][y] = d[t.first][t.second] +1;
q.push({x,y});
}
}
}
return d[n-1][m-1];
}
int main(){
cin>>n>>m;
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
cin>>g[i][j];
}
}
cout<<bfs();
return 0;
}
八数码(华容道)
思路:
- 广度优先搜索找可行解
- swap进行状态转移
- 每次转移distance + 1
#include<bits/stdc++.h>
using namespace std;
int n,m;
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1};
string s, end = "12345678x";;
int bfs(string state){
queue<string>q; // 队列(实现BFS)
unordered_map<string, int>d; // 哈希表 存储每种状态的distance
q.push(state); //初始化
d[state]=0;
while(q.size()){
auto t = q.front(); //每次遍历弹出队列中第一个元素
q.pop();
if(t==end) return d[t]; //终止条件
int distance = d[t];
int k = t.find('x'); //模拟3*3
int a = k/3, b= k%3;
for(int i=0;i<4;++i){
int x = a +dx[i];
int y = b +dy[i];
if(x>=0 &&x<3 &&y>=0 && y<3){
swap(t[x*3 +y],t[k]); //状态转移
if(!d.count(t)){ // 是否最早到达
d[t] = distance+1;
q.push(t);
}
swap(t[x*3 +y],t[k]);
}
}
}
return -1;
}
int main(){
char s[2];
string state;
for(int i=0;i<9;++i){
cin>>s;
state += *s;
}
cout<<bfs(state)<<endl;
return 0;
}
图/树的建立(邻接表)
void add(int a, int b){
/* idx:当前结点的编号(id), e[]:存储结点的值(b),
ne[]:指向下一个节点id的指针, h[]: 表头*/
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
树和图的DFS
树的重心
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n, h[N], e[N*2], ne[N*2], idx;
int ans = N;
bool st[N];
void add(int a,int b){
e[idx]=b;
ne[idx]= h[a];
h[a]= idx++;
}
int dfs(int u){
st[u] = true;
int size =0,sum =0; //初始化叶子结点,没有子块,size表示u结点单个子树的值
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(st[j]) continue;
int s = dfs(j); //取得当前u的子节点j的子树林节点和
size = max(size,s);
sum += s;
}
size = max(size,n-sum-1);
ans = min(ans,size); // 答案
return sum+1;// //返回u节点+所有u节点统领的节点的综合给下一次递归
}
int main(){
cin>>n;
memset(h,-1,sizeof(h));
for(int i=1;i<n;++i){
int a,b;
cin>>a>>b;
add(a,b); add(b,a);
}
dfs(1);
cout<<ans;
return 0;
}
树和图的BFS
- 用队列和邻接表存储操作,根据BFS层序遍历,可以发现元素第一次被遍历到的距离即为其到根节点的最小距离。
AcWing 847. 图中点的层次
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int h[N],ne[N],e[N],idx,n,m;
int d[N];
void add(int a, int b){
e[idx]=b, ne[idx] =h[a]; h[a] = idx++;
}
int bfs(){
memset(d, -1, sizeof d);
queue<int> q;
d[1]=0; q.push(1); // 初始化
while(q.size()){ //每次从队列中弹出队首元素
auto t = q.front(); q.pop();
for(int i = h[t];i !=-1;i=ne[i]){ //邻接表的遍历
int j = e[i];
if(d[j] == -1){ /*如果j结点还未被遍历过
(否则根据bfs原理,j第一次被遍历到的距离即为最小距离*/
d[j]= d[t]+1;
q.push(j);
}
}
}
return d[n];
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b;
cin>>a>>b;
add(a,b);
}
cout<<bfs();
}
最短路
朴素dijkstra
- 复杂度
0(N^2)
,适合稠密图,详见代码及注释
#include<bits/stdc++.h>
using namespace std;
const int N = 505;
bool st[N]={false};
int g[N][N], dist[N],n,m;
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;
for(int j=1;j<=n;++j) //找到t之后,更新t周围边的最小值
dist[j] = min(dist[j], dist[t]+g[t][j]);
st[t] = true;
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin>>n>>m;
memset(g,0x3f,sizeof g);
int n1,n2,d;
while(m--){
cin>>n1>>n2>>d;
g[n1][n2] = min(g[n1][n2],d);
}
cout<<dijkstra();
return 0;
}
堆优化的dijkstra算法
- 用优先队列代替朴素dijkstra算法中“寻找t”的过程, 复杂度为
O(mlogn)/O(mlogm)
。
#include<bits/stdc++.h>
using namespace std;
const int N = 150005;
typedef pair<int,int> pii;
int e[N],ne[N],h[N],n,m,idx,w[N],dist[N];
bool st[N];
void add(int a, int b, int c){
e[idx]=b; w[idx]=c; ne[idx]=h[a]; h[a] = idx++;
}
int dijkstra(){
memset(dist,0x3f, sizeof dist);
dist[1] =0;
priority_queue<pii , vector<pii>, greater<pii>> heap;
heap.push({0,1}); //堆中元素按照first(dist)排序
while(heap.size()){
auto t = heap.top(); heap.pop();
int ver = t.second;
if(st[ver]) continue;
st[ver]=true;
for(int i = h[ver];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[ver]+w[i]){
dist[j]= dist[ver]+w[i];
heap.push({dist[j],j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin>>n>>m;
int a,b,c;
memset(h,-1,sizeof h);
while(m--){
cin>>a>>b>>c;
add(a,b,c);
}
cout<<dijkstra();
}
带负权的最短路
bellman_ford(不如SPFA)
#include<bits/stdc++.h>
using namespace std;
const int N = 510, M= 10010;
struct Edge{
int a,b,c;
}edges[M];
int n,m,k,dist[N],last[N];
void bellman_ford(){
memset(dist, 0x3f, sizeof dist);
dist[1]=0;
for(int i=0;i<k;++i){
memcpy(last, dist, sizeof dist);
for(int j=0;j<m;++j){
auto e = edges[j];
dist[e.b] = min(dist[e.b] , last[e.a] +e.c);
}
}
}
int main(){
cin>>n>>m>>k;
int a,b,c;
for(int i=0;i<m;++i){
cin>>a>>b>>c;
edges[i] = {a,b,c};
}
bellman_ford();
if(dist[n]>0x3f3f3f3f /2 ) puts("impossible");
else cout<<dist[n];
return 0;
}
SPFA求最短路
- 曾经加入过队列的点出队后,会再次被加入队列?再次加入后继续更新与它联通的点
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int n,m, h[N],w[N],e[N],ne[N],idx;
int dist[N];
bool st[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int spfa(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size()){
int 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]){
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b,c; cin>>a>>b>>c; add(a,b,c);
}
int t = spfa();
if(t==0x3f3f3f3f) puts("impossible");
else cout<<t;
return 0;
}
判断图中是否有负权环
- 在spfa算法的基础上加一个计数数组即可
#include<bits/stdc++.h>
using namespace std;
const int N = 2010, M = 10010;
int n,m;
int h[N],w[M],e[M],ne[M],idx;
int dist[N], cnt[N];
bool st[N];
void add(int a,int b, int c){
e[idx]=b,w[idx]=c, ne[idx]=h[a],h[a] = idx++;
}
bool spfa(){
queue<int> q;
for(int i=1;i<=n;++i){
st[i]=true;
q.push(i);
}
while(q.size()){
int 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;
if(!st[j]){
q.push(j);
st[j] =true;
}
}
}
}
return false;
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa()) puts("Yes");
else cout<<"No";
}
拓扑排序 - 判断图中是否有环
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int n,m,h[N],e[N],ne[N],idx;
int d[N]; //度
int q[N];
void add(int a, int b){ //a -> b
e[idx]=b; ne[idx] =h[a]; h[a] = idx++;
}
bool transport(){
int hh=0, tt=-1; //hh指向头元素,可直接取,tt指向末尾元素
for(int i=1;i<=n;++i){
if(!d[i]) q[++tt] =i;
}
while(hh<=tt){
int t = q[hh++]; //取队列头的元素,其入度为0
for(int i=h[t];i!=-1;i=ne[i]){
//遍历这个点所有的子节点,将子节点的入度减一,
//同时判断子节点的入度是否为0,如果为0就进入队列
int j=e[i];
if(--d[j]==0) q[++tt]=j;
}
}
return tt ==n-1;
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=0;i<m;++i){
int a,b; cin>>a>>b;
add(a,b);
d[b]++; // degree
}
if(!transport()) puts("-1");
else {
for(int i=0;i<n;++i) cout<<q[i]<<" ";
}
}
最小生成树
Kruskal 算法
- 按边做,复杂度为
O(mlogm)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10, M =2e6+10, INF = 0x3f3f3f3f;
int n,m,p[N];
struct Edge{
int a,b,w;
}edges[M];
bool cmp(struct Edge A, struct Edge B){
return A.w<B.w;
}
int find(int x){ //并查集
if(p[x]!=x) p[x] = find(p[x]);
return p[x];
}
int kruskal(){
sort(edges,edges+m,cmp); //将边按权重升序排序
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){ //判断a、b是否连通
p[a] =b;
res +=w; //记录当前权重之和
cnt++; //记录当前生成树的边数之和
}
}
if(cnt<n-1) return INF; //不存在最小生成树
return res;
}
int main(){
cin>>n>>m;
for(int i=0;i<m;++i){
int a,b,w;
cin>>a>>b>>w;
edges[i] ={a,b,w};
}
int t = kruskal();
if(t==INF) puts("impossible");
else cout<<t;
}