数据结构期末复习,理一些模板一些题

开个新坑,距离数据结构期末考试 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+1x的大小,如果小,就交换
  • 对于down操作从哪里开始 由于最大元素是n,所以从n/2+1n的元素都位于堆的最后一排,因此不需要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;
}
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值