【记录】算法 - 算法基础c++

一、基础算法

(1)排序

快速排序

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);// 找到第一个>=x的数
        do j--; while(q[j] > x);// 找到第一个<=x的数
        if(i < j) swap(q[i], q[j]);
    }
    // (l, i)为<=x的数,(j, r)为>=x的数
    // quick_sort(q, l, i - 1), quick_sort(q, i, r);// 注意x=q[l]存在边界问题,死循环
    quick_sort(q, l, j), quick_sort(q, j+1, r);// 注意x=q[r]存在边界问题,死循环
}
// AcWing 786. 第k个数
// 用快排思想划分
#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int a[N];
int n, k;

int quick_sort(int l, int r, int k){
    if(l >= r) return a[l];
    int i = l - 1, j = r + 1, x = a[l];
    while(i < j){
        do i ++; while(a[i] < x);
        do j --; while(a[j] > x);
        if(i < j) swap(a[i], a[j]);
    }
    int sl = j - l + 1;// <=x的元素个数
    if(sl >= k) return quick_sort(l, j, k);
    return quick_sort(j+1, r, k-sl);
}// 时间复杂度O(n),n+n/2+n/4+....

int main(){
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    printf("%d", quick_sort(0, n-1, k));
    return 0;
}

归并排序

void merge_sort(int q[], int l, int r){
    if(l >= r) return;
    
    int m = l + r >> 1;
    merge_sort(q, l, m), merge_sort(q, m+1, r);

    int k = 0, i = l, j = m + 1; // 合并
    while(i <= m && j <= r)
        if(q[i] <= q[j]) temp[k++] = q[i++];
        else temp[k++] = q[j++];
        
    while(i <= m) temp[k++] = q[i++];
    while(j <= r) temp[k++] = q[j++];
    
    for(i = l, j = 0; i <= r; i++, j++) q[i] = temp[j];
}
// AcWing 788. 逆序对的数量
// 一边归并排序,一边计算逆序对数量
#include <iostream>
using namespace std;

typedef long long ll;

const int N = 1e5 + 10;

int q[N], tmp[N];
int n;

ll merge_sort(int l, int r){
    if(l >= r) return 0;
    
    int m = l + r >> 1;
    ll res = merge_sort(l, m) + merge_sort(m+1, r);// 逆序对只在左边or只在右边
    
    int k = 0, i = l, j = m + 1;
    while(i <= m && j <= r){
        if(q[i] <= q[j]) tmp[k++] = q[i++];
        else{
            tmp[k++] = q[j++];
            res += m - i + 1;// 逆序对一个在左,一个在右;与当前j构成的逆序对数量=i~m的个数(qi~qm都>qj)
        }
    }
    
    while(i <= m) tmp[k++] = q[i++];
    while(j <= r) tmp[k++] = q[j++];
    
    for(int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
    
    return res;
}

int main(){
    cin >> n;
    for(int i = 0; i < n; i ++) cin >> q[i];
    
    cout << merge_sort(0, n-1);
    
    return 0;
}

堆排序

// 常见堆操作
void down(int u){
    int t = u;
    // 当左孩子存在,且左孩子比u还小
    if(u*2 <= cnt && h[u*2] < h[t]) t = u * 2;
    // 当右孩子存在,且右孩子比t还小
    if(u*2+1 <= cnt && h[u*2+1] < h[t]) t = u * 2 + 1;
    // 最小的不是节点u
    if(t != u){
        swap(h[u], h[t]);
        down(t);
    }
}

void up(int u){
	// 当爹存在,且u比爹小
	while(u/2 && h[u/2] > h[u]){
		swap(h[u/2], h[u]);
		u /= 2;
	}
}
// Acwing 838. 堆排序
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;
int h[N];// 小根堆,从下标为1开始存储
int n, m, cnt;// cnt记录堆的大小

void down(int u){
    int t = u;
    // 当左孩子存在,且左孩子比u还小
    if(u*2 <= cnt && h[u*2] < h[t]) t = u * 2;
    // 当右孩子存在,且右孩子比t还小
    if(u*2+1 <= cnt && h[u*2+1] < h[t]) t = u * 2 + 1;
    // 最小的不是节点u
    if(t != u){
        swap(h[u], h[t]);
        down(t);
    }
}


int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &h[i]);
    cnt = n;
    
    // 从n/2的地方(倒数第二层)开始down,时间复杂度降到O(n)
    for(int i = n/2; i; i--) down(i);
    
    while(m--){
        printf("%d ", h[1]);
        h[1] = h[cnt--];
        down(1);
    }
    return 0;
}
// Acwing 839. 模拟堆
#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int h[N];// 值,小根堆
int ph[N];// ph[k] = x,第k个插入的元素在堆中存放的位置x
int hp[N];// hp[x] = k,堆中存放的位置x是第k个插入的
int cnt = 0, cnt_i = 0;

void heap_swap(int x, int y){
    swap(ph[hp[x]], ph[hp[y]]);
    swap(hp[x], hp[y]);
    swap(h[x], h[y]);
}
void up(int u){
    while(u/2 && h[u/2] > h[u]){
        heap_swap(u/2, u);
        u /= 2;
    }    
}
void down(int u){
    int t = u;
    if(u*2<=cnt && h[u*2]<h[t]) t = u*2;
    if(u*2+1<=cnt && h[u*2+1]<h[t]) t = u*2+1;
    if(t != u){
        heap_swap(t, u);
        down(t);
    }
}

int main(){
    int n, k, x; cin >> n;
    string op;
    while(n--){
        cin >> op;
        if(op == "I"){
            cin >> x;
            h[++cnt] = x;
            ph[++cnt_i] = cnt;
            hp[cnt] = cnt_i;
            up(cnt);
        }else if(op == "PM"){
            cout << h[1] << endl;
        }else if(op == "DM"){
            heap_swap(1, cnt--);
            down(1);
        }else if(op == "D"){
            cin >> k;
            x = ph[k];
            heap_swap(ph[k], cnt--);
            up(x);
            down(x);
        }else{
            cin >> k >> x;
            h[ph[k]] = x;
            up(ph[k]);
            down(ph[k]);
        }
    }
    
    return 0;
}

(2)二分

看到最大(小)…里的最小(大),就要想起二分!

整数二分

在这里插入图片描述

// 找右边界(1、红色部分)
int bsearch1(int l, int r){
	while(l < r){
		int mid = (l + r + 1) >> 1;// +1防止死循环
		if(check(mid)) l = mid;
		else r = mid - 1;
	}
	return l;
}
// 找左边界(2、绿色部分)
int bsearch2(int l, int r){
	while(l < r){
		int mid = (l + r) >> 1;
		if(check(mid)) r = mid;
		else l = mid + 1;
	}
	return l;
}

浮点数二分

double bsearch(double l, double r){
	const double eps = 1e-8;// 题目要求n位小数则多精确两位
	while(r - l > eps){
		double mid = (l + r) / 2;
		if(check(mid)) r = mid;// 不用考虑边界
		else l = mid;
	}
	return l;
}
// AcWing 790. 数的三次方根
#include <iostream>
using namespace std;

int main(){
    double x; cin >> x;
    
    double l = -10000, r = 10000;
    while(r - l > 1e-8){
        double m = (l + r) / 2;
        if(m * m * m >= x) r = m;
        else l = m;
    }
    
    printf("%lf\n", l);
    return 0;
}

(3)高精度

A+B

vector<int> add(vector<int> &A, vector<int> &B){
	vector<int> c;
	int t = 0;// 本位和
	for(int i = 0; i < A.size() || i < B.size(); i++){
		if(i < A.size()) t += A[i];
		if(i < B.size()) t += B[i];
		c.push_back(t % 10);
		t /= 10;
	}
	if(t) c.push_back(1);
	return c;
}

A-B

// 比较A是否>B
bool cmp(vector<int> &A, vector<int> &B){
	if(A.size() != B.size()) return A.size() > B.size();
	for(int i = A.size() - 1; i >= 0; i--)
		if(A[i] != B[i]) return A[i] > B[i];
	return true;
}
// 默认A>B
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);
		t = t < 0 ? 1 : 0;// 借位
	}
	while(c.size() > 1 && c.back() == 0) c.pop_back();// 去掉前导0
	return c;
}

Axb

vector<int> mul(vector<int> &A, int b){
	vetcor<int> c;
	for(int i = 0, t = 0; i < A.size() || t; i ++){ // 最后t进位
		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

// 余数r通过引用传递
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;
}

(4)前缀和与差分

一维前缀和

S(l-1) = a1 + a2 + … + a(l-1)
Sr = a1 + a2 + … + a(l-1) + al + … + ar
区间和S[l, r] = Sr - S(l-1)

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int n, m;
int a[N], s[N];

int main(){
	// 数据>1e6用scanf,否则cin
    // ios::sync_with_stdio(false);// 取消cin同步,加快cin读取速度,但不能使用scanf
    
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]); // 其实没必要存a[i]
    
    for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];// 求前缀和
    
    while(m--){
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l-1]);// 求部分和
    }
    return 0;
}

二维前缀和

// AcWing 796. 子矩阵的和
#include <iostream>
using namespace std;

const int N = 1010;
int n, m, q;
int a[N][N], s[N][N];

int main (){
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            scanf("%d", &a[i][j]); // 其实没必要存a[i][j]
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];// 求前缀和
    
    while(q--){
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]);// 求部分和
    }
    return 0;
}

一维差分

a1, a2, … , ai
构造ai = b1 + b2 + … + bi,数组b为a的差分,数组a为b的前缀和。

// AcWing 797. 差分
#include <iostream>

const int N = 1e5 + 10;
int n, m;
int a[N], b[N];// a前缀和,b差分

// 使得a[l]+c, ..., a[r]+c
void insert(int l, int r, int c){
    b[l] += c;
    b[r+1] -= c;
}

int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]); // 可以合并操作
    
    // 初始化差分,相当于在[i,i]使得a[i] += c
    for(int i = 1; i <= n; i++) insert(i, i, a[i]);
    
    while(m--){
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
    
    for(int i = 1; i <= n; i++) b[i] += b[i-1];// 求前缀和
    for(int i = 1; i <= n; i++) printf("%d ", b[i]);
    
    return 0;
}

二维差分

// AcWing 798. 差分矩阵
#include <iostream>
using namespace std;

const int N = 1010;
int n, m, q;
int a[N][N], b[N][N];

void insert(int x1, int y1, int x2, int y2, int c){
    b[x1][y1] += c;
    b[x2+1][y1] -= c;
    b[x1][y2+1] -= c;
    b[x2+1][y2+1] += c;
}

int main(){
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            scanf("%d", &a[i][j]);
            
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            insert(i, j, i, j, a[i][j]);
            
    while(q--){
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        insert(x1, y1, x2, y2, c);
    }
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            b[i][j] += b[i-1][j] +  b[i][j-1] - b[i-1][j-1];// 求前缀和
            
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++) printf("%d ", b[i][j]);
        puts("");
    }
    
    return 0;
}

(5)双指针

把O(n^2)的朴素算法优化到O(n)

for(i = 0, j = 0; i < n; i++){
	while(j < i && check(i, j)) j++;
	...
}
// AcWing 799. 最长连续不重复子序列
#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int n;
int a[N];
int s[N];// s[i]表示i出现的次数

int main(){
    cin >> n;
    for(int i = 0; i < n; i++) cin >> a[i];
    
    int res = 0;
    for(int i = 0, j = 0; i < n; i++){
        s[a[i]] ++;// a[i]加入序列
        while(s[a[i]] > 1){// j~i序列里有重复元素
            s[a[j]]--;
            j++;// j向前移动
        }
        res = max(res, i - j + 1);
    }
    cout << res << endl;
    
    return 0;
}

(6)位运算

n >> k & 1

  • 表示n的二进制表示第k位(从右往左)上的数字

lowbit(n)

  • = n & -n(n&n的补码,-n=~n+1)
  • 返回n的二进制表示最右一个1(1010返回0010)
// AcWing 801. 二进制中1的个数
#include <iostream>
using namespace std;

int n;
int lowbit(int x){
    return x & -x;
}
int main(){
    cin >> n;
    for(int i = 1; i <= n; i++){
        int x; cin >> x;
        int cnt = 0;
        while(x) x -= lowbit(x), cnt ++; 
        cout << cnt << " ";
    }
    return 0;
}

(7)离散化

数据值域跨度大,但用到的不多

// 存储所有待离散的值
vector<int> alls;
// 排序
sort(alls.begin(), alls.end());
// 去除重复元素
alls.erase(unique(alls.begin(), alls.end()), alls.end());

// 二分找出x对应的离散化的值(返回第一个>=x的位置)
int find(int x){
	int l = 0, r = alls.size()-1;
	while(l < r){
		int m = l + r >> 1;
		if(alls[m] >= x) r = mid;
		else l = mid + 1;
	}
	return r + 1;// 映射到1, 2, ...n
}
// AcWing 802. 区间和
 #include <iostream>
 #include <vector>
 #include <algorithm>
 using namespace std;
 
 typedef pair<int, int> PII;
 const int N = 300000 + 10;
 
 // 把插入and查询操作的下标alls 离散化处理映射到 a,不离散会超时,因为要开2*10^9大小的数组
 int a[N], s[N];// 数(映射后,总大小为n+2m=300000),前缀和
 vector<int> alls;// 所有的插入和查询操作的下标(待映射的n+2m个坐标)
 vector<PII> add, query;// 插入操作,查询操作
 int n, m;
 
 int find(int x){
     int l = 0, r = alls.size()-1;
     while(l < r){
         int mid = l + r >> 1;
         if(alls[mid] >= x) r = mid;
         else l = mid + 1;
     }
     return r + 1;// 使用前缀和,下标+1
 }
 
 int main(){
     cin >> n >> m;
     for(int i = 0; i < n; i ++){
         int x, c;
         cin >> x >> c;
         add.push_back({x, c});
         
         alls.push_back(x);
     }
     
     for(int i = 0; i < m; i ++){
         int l, r;
         cin >> l >> r;
         query.push_back({l, r});
         
         alls.push_back(l);
         alls.push_back(r);
     }
     
     // 去重
     sort(alls.begin(), alls.end());
     alls.erase(unique(alls.begin(), alls.end()), alls.end());
     
     // 插入
     for(auto item : add){
        int x = find(item.first);// 找到去重后映射的下标
        a[x] += item.second; 
     }
     // 处理前缀和,这里的alls.size()是去重后的
     for(int i = 1; i <= alls.size(); i ++) s[i] = s[i-1] + a[i];
     
     // 询问
     for(auto item : query){
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l-1] << endl;
     }
     
     return 0;
 }

(8)区间合并

void merge(vector<PII> &segs){
	vector<PII> res;
	sort(segs.begin(), segs.end());

	int st = -2e9, ed = -2e9;
	for(auto seg : segs)
		if(seg.first > ed){
			if(st != -2e9) res.push_back({st, ed});
			st = seg.first, ed = seg.second;
		}
		else ed = max(ed, seg.second);
	
	if(st != -2e9) res.push_back({st, ed});// 把最后的区间加上
	segs = res;
}
// AcWing 803. 区间合并
// 类似会议安排
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

typedef pair<int, int> PII;

vector<PII> segs;
int n;

int main(){
    cin >> n;
    while(n--){
        int l, r;
        cin >> l >> r;
        segs.push_back({l, r});
    }
    // pair排序默认以pair.first
    sort(segs.begin(), segs.end());
    
    int cnt = 0, ed = -2e9;
    for(auto item : segs){// 不需要知道区间的具体值,所以只需要更新ed
        if(item.first > ed) cnt ++;
        ed = max(ed, item.second);
    }
    cout << cnt << endl;
    return 0;
}

(9)递归

// 汉诺塔问题
#include <iostream>
using namespace std;

void move(int n, char from, char to){ // 把编号为n的盘子从from移到to
    printf("%d: %c -> %c\n", n, from, to);
}

void hanoi(int n, char A, char B, char C){ // 把n个盘子从A移到C,B是辅助盘子
    if(n == 1){
        move(n, A, C);
        return;
    }
    hanoi(n-1, A, C, B); // 把上面n-1个盘子从A移到B
    move(n, A, C); // 把最底下的盘子从A移到C
    hanoi(n-1, B, A, C); // 把n-1个盘子从B移到C
}

int main(){
    int n; cin >> n;
    hanoi(n, 'A', 'B', 'C');
}

二、数据结构

(1)链表与邻接表

单链表

用静态链表模拟单链表

// AcWing 826. 单链表
#include <iostream>
using namespace std;

const int N = 100010;

// head表示头指针
// e[i]表示节点i的值
// ne[i]表示节点i的next指针
// idx表示尾节点的下一个未使用的位置
int head, e[N], ne[N], idx;

// 初始化
void init(){
    head = -1;
    idx = 0;
}

// 将x插到头节点
void add_to_head(int x){
    e[idx] = x, ne[idx] = head, head = idx++;
}

// 将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]];
}

int main(){
    int m; cin >> m;
    init();
    
    while(m--){
        char op;
        int k, x;
        
        cin >> op;
        if(op == 'H'){
            cin >> x;
            add_to_head(x);
        }else if(op == 'D'){
            cin >> k;
            if(k) remove(k-1);
            else head = ne[head];// 删除头节点
        }else{
            cin >> k >> x;
            add(k-1, x);
        }
    }
    
    for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
    
    return 0;
}

双链表

// AcWing 827. 双链表
#include <iostream>
using namespace std;

const int N = 100010;
int m;
int e[N], l[N], r[N], idx;

void init(){
    // 下标为0的点做左边界点,下标为1的点做右边界点
    r[0] = 1; l[1] = 0;
    // 初始idx从2开始
    idx = 2;
}

// 将x插入到下标为a的点右边
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];
}

int main(){
    cin >> m;
    
    init();
    
    while(m--){
        string op;cin >> op;
        int k, x;
        if(op == "L"){
            cin >> x;
            insert(0, x);
        }else if(op == "R"){
            cin >> x;
            insert(l[1], x);
        }else if(op == "D"){
            cin >> k;
            remove(k+1);
        }else if(op == "IL"){
            cin >> k >> x;
            insert(l[k+1], x);
        }else{
            cin >> k >> x;
            insert(k+1, x);
        }
    }
    
    for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << " ";
    
    return 0;
}

邻接表

见树与图

(2)栈与队列

用数组模拟栈

// tt栈顶
int stk[N], tt = -1;

// 插入x
stk[++t] = x;

// 弹出栈顶
tt--;

// 栈顶的值
stk[tt];

// 判断栈是否为空
if(tt == -1) return true;
else false;

// 判断栈是否为满
if(tt == N-1) return true;
else false;

// 栈的长度
tt + 1;

队列

用数组模拟队列

// 普通队列
// hh队头,tt队尾
int q[N], hh = 0, tt = -1;

// 插入
q[++tt] = x;

// 队头出队
hh++;

// 队头的值
q[hh];

// 判断队列是否为空
if(hh <= tt) return false;
else return true; 





// 循环队列
// hh队头,tt队尾的下一个位置
int q[N], hh = 0, tt = 0;

// 插入
q[tt] = x;
tt = (tt + 1) % N;

// 从队头弹出
hh = (hh + 1) % N;

// 队头的值
q[hh];

// 判断队列是否为空
if(hh == tt) return true;
else return false;

// 判断队列是否为满(以队头在队尾指针的下一位作为标志)
if((tt+1)%N == hh) return true;
else return false;

// 队列长度
(hh - tt + N) % N

单调栈

// AcWing 830. 单调栈
// 输出每个数左边第一个比它小的数
#include <iostream>
using namespace std;

const int N = 100010;
int n;
int stk[N], tt;

int main(){
    cin >> n;
    while(n--){
        int x; cin >> x;

        // 当栈不为空and栈顶元素>=x,栈顶弹出(保持栈顶为最近的最小值)
        while(tt && stk[tt] >= x) tt--;
        
        if(tt) cout << stk[tt] << " ";
        else cout << "-1 ";

		// 当前x入栈
        stk[++tt] = x;
    }
    return 0;
}

单调队列

// AcWing 154. 滑动窗口
#include <iostream>
using namespace std;

const int N = 1e6+10;

int a[N], q[N];// a保存数值;q队列模拟滑动窗口,保存下标
int hh = 0, tt = -1;
int n, k;


int main(){
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    // 找滑动窗口的最小值
    for(int i = 0; i < n; i++){
        // 队头元素不在窗口内(队列非空&&队头在数组中的下标<当前窗口的最小下标),队头出队
        // 用if是因为每次只有一个元素进队,一个元素出队
        if(hh <= tt && q[hh] < i-k+1) hh++;
        
        // 队列非空&&队尾>=当前元素,队尾出队(保持队列单调递增,队头为当前窗口最小值)
        while(hh <= tt && a[q[tt]] >= a[i]) tt--;
        
        // 当前i入队
        q[++tt] = i;
        
        // 凑足k个数才开始输出
        if(i >= k - 1) printf("%d ", a[q[hh]]);
    }
    
    puts("");
    
    hh = 0, tt = -1;
    // 找滑动窗口的最大值
    for(int i = 0; i < n; i++){
        if(hh <= tt && q[hh] < i-k+1) hh++;
        while(hh <= tt && a[q[tt]] <= a[i]) tt--;
        q[++tt] = i;
        if(i >= k - 1) printf("%d ", a[q[hh]]); 
    }
    
    return 0;
}

(3)kmp

// 朴素模式匹配
// 时间复杂度O(nm)
void search(string p, string s){
	// 模式串p,字符串s
	// 从下标1开始匹配
	// i指向字符串,j指向模式串,两个一起移动
	for(int i = 1; i <= s.size(); i++){
		bool flag = true;
		for(int j = 1; j <= p.size(); j++){
			if(s[i+j-1] != p[j]){// 注意是i + j-1,不是j
				flag = false;
				break;
			}
		}
	}
}

// kmp
// 时间复杂度O(n+m)
// 利用已知s串后缀匹配p串的情况,减少s串在匹配p串时存在的回溯的情况
#include <iostream>
using namespace std;

const int N = 1e5+10, M = 1e6+10;
char p[N], s[M];
int ne[N];
int n, m;

int main(){
	// p和s从下标1开始存储
    cin >> n >> p+1 >> m >> s+1;
    
    // 求next数组
    // i从2开始,因为ne[1]恒=0;ne[i]=j表示模式串p的[i-j+1~i]=[1~j](后缀=前缀),即从模式串p的i位置开始,最大匹配长度j
    for(int i = 2, j = 0; i <= n; i++){
        while(j && p[i] != p[j+1]) j = ne[j]; // 不断尝试扩展匹配长度j,如果扩展失败,回溯j=next[j],直到j=0
        if(p[i] == p[j+1]) j++; // 如果匹配成功,j长度+1
        ne[i] = j; 
    }
    
    // 匹配字符串
    // i从1开始,j从0开始
    for(int i = 1, j = 0; i <= m; i++){
        // s[i]和p[j+1]是否匹配,不匹配,j递归=ne[j],不断地使s串的后缀=p串的前缀,直到s[i]=p[j+1]或者j=0
        while(j && s[i] != p[j+1]) j = ne[j];
        // 匹配,j往后移一位
        if(s[i] == p[j+1]) j++;
        // 完全匹配
        if(j == n) printf("%d ", i-n);
    }
    return 0;
}

(4)前缀树Trie

// AcWing 835.Trie字符串统计
#include <iostream>
using namespace std;

const int N = 1e5 + 10;// 字符串长度

// son[x][y]=z表示节点x第y个儿子下标为z
// cnt[]表示以该节点结尾的单词有几个
// idx表示当前用到哪个节点
int son[N][26], cnt[N], idx;

int n;
char str[N];

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 main(){
    cin >> n;
    while(n--){
        char op;
        cin >> op >> str;
        if(op == 'I') insert(str);
        else cout << query(str) << endl;
        // 如果是用scanf,建议用%s配合char[]读;用%c会把前一行的换行、空格读入
        /*
		    char op[2];
		    scanf("%s%s", op, str);
		    if(*op == "I") ...
        */
        
    }
    return 0;
}
// AcWing 143. 最大异或对
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10, M = 31 * N;

int a[N], son[M][2], idx;
int n;

void insert(int x){
    int p = 0;
    for(int i = 30; i >= 0; i--){
        int u = x >> i & 1;// 第i+1位上的数字
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
}

int query(int x){
    int p = 0, res = 0;// res记录和当前x异或后结果的最大值
    for(int i = 30; i >= 0; i--){
        int u = x >> i & 1;
        if(son[p][!u]){// 从最高位开始,每次尽量走与当前位相反的路
            p = son[p][!u];
            res = res*2 + 1;
        }else{
            p = son[p][u];
            res = res*2;
        } 
    }
    return res;
}

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        scanf("%d", &a[i]);
        insert(a[i]);
    }
    
    int res = 0;
    for(int i = 0; i < n; i ++) res = max(res, query(a[i]));
    
    printf("%d", res);
    return 0;
}

(5)并查集

#include <iostream>
using namespace std;

const int N = 1e5 + 10;

int p[N];
int n, m;

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);// 路径压缩
    return p[x];
}

int main(){
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++) p[i] = i;
    
    while(m--){
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        if(*op == 'M') p[find(a)] = find(b);
        else{
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}
// AcWing 837. 连通块中点的数量
#include <iostream>
using namespace std;

const int N = 1e5 + 10;

int p[N], cnt[N];
int n, m;

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);// 路径压缩
    return p[x];
}

int main(){
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++) p[i] = i, cnt[i] = 1;
    
    while(m--){
        char op[5];
        int a, b;
        scanf("%s", op);
        
        if(op[0] == 'C'){
            scanf("%d%d", &a, &b);
            a = find(a), b = find(b);
            if(a == b) continue;// 注意a和b可能已经在同一个集合里
            p[a] = b;
            cnt[b] += cnt[a];
        }else if(op[1] == '1'){
            scanf("%d%d", &a, &b);
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }else{
            scanf("%d", &a);
            printf("%d\n", cnt[find(a)]);
        }
    }
    return 0;
}
// Acwing 240. 食物链
#include <iostream>
using namespace std;

const int N = 5e5 + 10;
int n, k;
int p[N], d[N];
// d[i]表示节点i到根结点的距离, 用距离来表示节点和根结点之间的关系
// 其中距离为1表示可以吃根结点;距离为2表示可以被根结点吃;距离为3表示和根结点是同类

int find(int x){
    if(p[x] != x){
        int t = find(p[x]);// 确保先调用find(p[x]),更新d[p[x]]
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main(){
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) p[i] = i;
    
    int res = 0;
    while(k--){
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);
        
        if(x > n || y > n) res++;
        else{
            int px = find(x), py = find(y);
            if(t == 1){// 判断是否为同类
                // 如果x和y在同一个集合里 并且 x、y到根结点的距离差mod3不等于0,说明x和y不是同类
                if(px == py && (d[x] - d[y]) % 3) res ++;
                // 如果x和y不在一个集合里,合并到一个集合里
                else if(px != py){
                    p[px] = py;
                    // 令x和y为同类,则(d[x]+?-d[y])mod3=0,则?=d[y]-d[x]
                    d[px] = d[y]-d[x];
                }
            }else{// 判断x是否吃y
                // 如果x和y在同一个集合里 并且 x、y到根结点的距离差mod3不等于1,说明x不吃y
                if(px == py && (d[x] - d[y] - 1) % 3) res ++;
                // 如果x和y不在一个集合里,合并到一个集合里并使x吃y
                else if(px != py){
                    p[px] = py;
                    // 令x吃y,则(d[x]+?-d[y]-1)mod3=0,则?=d[y]+1-d[x]
                    d[px] = d[y]+1-d[x];
                }
                
            }
        }
    }
    
    printf("%d", res);
    return 0;
}

(6)堆

(7)Hash表

开放寻址法

#include <iostream>
#include <cstring>
using namespace std;

const int N = 200003, INF = 0x3f3f3f3f;
// 开放寻址法中,hash表表长一般为表中元素个数的2~3倍的质数
int h[N];
int n;

// 要么返回x的位置,要么返回k后第一个空的位置。
int find(int x){
    int k = (x % N + N) % N;
    while(h[k] != INF && h[k] != x){
        k++; 
        if(k == N) k = 0;// 循环找
    }
    return k;
}

int main(){
    scanf("%d", &n);
    
    memset(h, 0x3f, sizeof(h));// 按字节赋值
    
    while(n--){
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        
        if(*op == 'I') h[find(x)] = x;
        else{
            if(h[find(x)] == INF) puts("No");
            else puts("Yes");
        }
    }
    return 0;
}

0x3f3f3f3f
在算法竞赛中,我们常常需要用到设置一个常量用来代表“无穷大”。
比如对于int类型的数,有的人会采用INT_MAX,即0x7fffffff作为无穷大。但是以INT_MAX为无穷大常常面临一个问题,即加一个其他的数会溢出。
而这种情况在动态规划,或者其他一些递推的算法中常常出现,很有可能导致算法出问题。
所以在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。0x3f3f3f3f主要有如下好处:
0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。
0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。

拉链法

#include <iostream>
#include <cstring>
using namespace std;

const int N = 100003;// hash表表长最好为质数
int h[N], e[N], ne[N], idx;
int n;

void insert(int x){
    int k = (x % N + N) % N;// 散列化,+N防止负数
    /*
	    一般余数应为正数。
	    在c++中,-10%3=-1;正确计算应该10=-4*3+2,-10%3=2。
	    不能(x+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 main(){
    scanf("%d", &n);
    
    memset(h, -1, sizeof(h));
    
    while(n--){
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        
        if(*op == 'I') insert(x);
        else{
            if(find(x)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

字符串哈希

在这里插入图片描述
在这里插入图片描述

// AcWing 841. 字符串哈希
#include <iostream>
using namespace std;

typedef unsigned long long ULL;// unsigned用来mod Q(=2^64)

const int N = 1e5 + 10, P = 131;// P为P进制,一般取131或者13331,不容易冲突

int n, m;
char str[N];
ULL h[N], p[N];// 哈希值前缀和;p[i]表示p^i

ULL get(int l, int r){
    return h[r] - h[l-1] * p[r-l+1]; // 前缀和
    /*
	    h[r]为1到r位字符串的哈希值
	    h[l-1]为1到l-1位字符串的哈希值
	    * p[r-l+1]为了让 1到l-1位 左边对齐到 1到r位
    */
}// 返回[l, r]区间内字符串的哈希值

int main(){
    scanf("%d%d", &n, &m);
    scanf("%s", str + 1);
    
    // 打表
    p[0] = 1;
    for(int i = 1; i <= n; i++){
        h[i] = h[i-1] * P + str[i];// str[i]自动转化为ascii码,128个
        p[i] = p[i-1] * P;
    }
    
    while(m--){
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        
        if(get(l1, r1) == get(l2, r2)) puts("Yes");
        else puts("No");
    }
    return 0;
}

三、搜索与图论

(1)DFS

时间复杂度O(n+m),空间复杂度O(n)

基本实现

递归

// AcWing 842. 排列数字
#include <iostream>
using namespace std;

const int N = 10;
int n;
int path[N];// path[i]表示第i位上的数字,从0开始
bool st[N];// st[i]表示i是否被用过

void dfs(int u){// u表示当前走到第i位
    // 递归结束的条件
    if(u == n){
        for(int i = 0; i < n; i++) printf("%d ", path[i]);
        puts("");
        return;
    }
    // 递归
    for(int i = 1; i <= n; i++)
        if(!st[i]){// 如果i还没有被使用过
            path[u] = i;
            st[i] = true;// 状态标记
            dfs(u+1);
            st[i] = false;// 递归后恢复
        }
}

int main(){
    scanf("%d", &n);
    
    dfs(0);
    
    return 0;
}
// AcWing 846. 树的重心
#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5 + 10, M = N * 2;

int n;
int h[N], e[M], ne[M], idx;
// 用邻接表表示(以边为主体节点)
// e[i],i是边编号,值是i这条边终点的结点编号
int ans = N;// 最终结果
bool st[N];// dfs标记,st[i]表示编号为i的节点是否遍历过

// 添加从a到b的单向边
void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 求以u为根的子树中包含的节点数量
int dfs(int u){
    st[u] = true;
    
    int sum = 1, res = 0;// 整棵树的节点数;删除该节点后子树中各连通块节点数最大值
    for(int i = h[u]; i != -1; i = ne[i]){// 遍历该节点的邻接表
        int j = e[i];
        if(!st[j]){// 如果j这个点没有搜过
            int s = dfs(j);
            res = max(res, s);
            sum += s;
        }
    }
    res = max(res, n - sum);// 比较节点的子树中各连通块节点数、除了以u节点为根的树以外的节点总数
    ans = min(ans, res);// 比较最小值
    return sum;
}

int main(){
    cin >> n;
    
    memset(h, -1, sizeof h);
    
    // 输入n-1条边
    for(int i = 0; i < n-1; i ++){
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    
    dfs(1);
    
    cout << ans << endl;
    
    return 0;
}

vector<vector<int>> adj; // 邻接表 
vector<bool> vis; // 记录节点是否已经遍历 
void dfs(int s) { 
	stack<int> st;
	st.push(s);
	vis[s] = true; 
	while (!st.empty()) { 
		int u = st.top(); 
		st.pop(); 
		for (int v : adj[u]) { 
			if (!vis[v]) {
				vis[v] = true; // 确保栈里没有重复元素 
				st.push(v); 
			} 
		} 
	}
}

连通性模型

状态是棋子不回溯,状态是棋盘要回溯。

// AcWing 1113. 红与黑
#include <iostream>
#include <cstring>
using namespace std;

const int N = 30;
char g[N][N];
int w, h, sx, sy;
bool st[N][N];


int dfs(int sx, int sy){
    int res = 1;
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

    st[sx][sy] = true;
    for(int i = 0; i < 4; i++){
        int a = sx + dx[i], b = sy + dy[i];
        if(a < 0 || a >= h || b < 0 || b >= w) continue;
        if(g[a][b] != '.') continue;
        if(st[a][b]) continue;
        
        res += dfs(a, b);
    }
    
    return res;
}

int main(){
    while(cin >> w >> h, w || h){
        for(int i = 0; i < h; i++)
            for(int j = 0; j < w; j++){
                cin >> g[i][j];
                if(g[i][j] == '@') sx = i, sy = j;
            }
            
        memset(st, 0, sizeof st);
        cout << dfs(sx, sy) << endl;
    }
    
    return 0;
}

搜索顺序

// AcWing 1116. 马走日
#include <iostream>
#include <cstring>
using namespace std;


const int N = 10;
int n, m, x, y;
bool st[N][N];
int res;   // 能遍历棋盘的途径总数

void dfs(int sx, int sy, int cnt){ //  cnt为当前在找第几个点
    if(cnt == n * m){
        res ++;
        return;
    }
    
    st[sx][sy] = true;
    int dx[8] = {-1, -2, -2, -1, 1, 2, 2, 1}, dy[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
    for(int i = 0; i < 8; i++){
        int a = sx + dx[i], b = sy + dy[i];
        if(a < 0 || a >= n || b < 0 || b >= m) continue;
        if(st[a][b]) continue;
        
        dfs(a, b, cnt+1);
    }
	st[sx][sy] = false; // 状态是棋盘需要恢复现场
}

int main(){
    int t; cin >> t;
    while(t--){
        cin >> n >> m >> x >> y;
        res = 0;
        
        dfs(x, y, 1);
        cout << res << endl;
    }
    return 0;
}
// AcWing 1118. 分成互质组
#include <iostream>
#include <cstring>
using namespace std;

const int N = 10;
int n;
int nums[N], g[N][N]; // g是组
bool st[N];
int ans = N;

int gcd(int a, int b){  // 求最大公约数
    return b ? gcd(b, a%b) : a;
}

bool check(int g[], int gc, int i){ // check i能否插入组内
    for(int j = 0; j < gc; j++)
        if(gcd(nums[g[j]], nums[i]) > 1) return false;
    return true;
}

void dfs(int u, int gc, int tc, int start){ // 当前搜第几个组,当前组里有几个元素,当前一共搜了几个元素,当前组从哪个下标开始搜
    if(u >= ans) return;
    if(tc == n) ans = u;
    
    bool flag = true;   // 当前组是否搜完,如果下面的for循环结束了都没找到一个元素说明当前组已经搜完
    for(int i = start; i < n; i++)
        if(!st[i] && check(g[u], gc, i)){
            g[u][gc] = i;
            st[i] = true;
            dfs(u, gc+1, tc+1, i+1);
            st[i] = false;
            
            flag = false;
        }
    
    if(flag) dfs(u+1, 0, tc, 0);
}


int main(){
    cin >> n;
    for(int i = 0; i < n; i ++) cin >> nums[i];
    
    dfs(1, 0, 0, 0);
    cout << ans << endl;
    
    return 0;
}

剪枝与优化

在这里插入图片描述

// AcWing 165. 小猫爬山
// 按分成互斥组的写法会TLE
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 20; 
int n, w; 
int c[N];
int sum[N]; // 每个组的重量
int ans = N;

void dfs(int u, int k){ //  当前安排第u只猫, 当前共有几个车可以安排
    // 最优性剪枝
    if(k >= ans) return;
    if(u == n){
        ans = k;
        return;
    }
    
    for(int i = 0; i < k; i++){ // 分支1:塞到某辆车
        if(sum[i] + c[u] <= w){ // 可行性剪枝
            sum[i] += c[u];
            dfs(u + 1, k);
            sum[i] -= c[u]; // 恢复现场
        }
    }
    
    // 分支2:新开一辆车
    sum[k] = c[u];
    dfs(u+1, k+1);
    sum[k] = 0; // 恢复现场
}

int main(){
    cin >> n >> w;
    for(int i = 0; i < n; i ++) cin >> c[i];
    
    sort(c, c + n); // 优化搜索顺序:优先搜索分支较少的节点(胖猫)
    reverse(c, c + n);
    
    dfs(0, 0);
    cout << ans << endl;
    
    
    return 0;
}
// AcWing 166. 数独
// 神代码
#include <iostream>
using namespace std;

const int N = 9, M = 1 << N;

int ones[M], map[M];
int row[N], col[N], cell[3][3]; // row[i]表示第i行的二进制表示(1表示可用,0表示不可用)
char str[100];

int lowbit(int x){
    return x & -x;
}

void init(){
    for(int i = 0; i < N; i++)
        row[i] = col[i] = (1 << N) - 1;
    
    for(int i = 0; i < 3; i++)
        for(int j = 0; j < 3; j++)
            cell[i][j] = (1 << N) - 1;
}

void draw(int x, int y, int t, bool is_set){    // 在(x,y)填上or删掉t
    if(is_set) str[x*N+y] = '1' + t;
    else str[x*N+y] = '.';
    
    int v = 1 << t;
    if(!is_set) v = -v;
    
    row[x] -= v;
    col[y] -= v;
    cell[x/3][y/3] -= v;
}

int get(int x, int y){  // 位运算优化,返回哪个数字能用
    return row[x] & col[y] & cell[x/3][y/3];
}

bool dfs(int cnt){
    if(!cnt) return true;
    
    int minv = 10;
    int x, y;
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++) // 优化搜索顺序:找到某个位置可填数字最少的
            if(str[i*N+j] == '.'){
                int state = get(i, j);
                if(ones[state] < minv){
                    minv = ones[state];
                    x = i, y = j;
                }
            }
    
    int state = get(x, y);  // 尝试填数字
    for(int i = state; i; i -= lowbit(i)){
        int t = map[lowbit(i)];
        draw(x, y, t, true);
        if(dfs(cnt-1)) return true;
        draw(x, y, t, false);
    }
    return false;
}

int main(){
    for(int i = 0; i < N; i++) map[1 << i] = i; // 打表map[2^i] = i
    for(int i = 0; i < 1 << N; i++) 
        for(int j = 0; j < N; j++) // 打表one[i] = i二进制表示里有几个1
            ones[i] += i >> j & 1;
    
    while(cin >> str, str[0] != 'e'){
        init();
        
        int cnt = 0; // 有几个位置已经填上数字
        for(int i = 0, k = 0; i < N; i++)
            for(int j = 0; j < N; j++, k++)
                if(str[k] != '.'){
                    int t = str[k] - '1';
                    draw(i, j, t, true);
                }else cnt ++;
              
              
        dfs(cnt);
        
        puts(str);
    }
    
    return 0;
} 
// AcWing 167. 木棒
// 好多细节
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 70;
int n;
int w[N], sum, length;
bool st[N];

bool dfs(int u, int cur, int start){    // 当前已经搜索几根木棒,当前组棒的长度,从哪根棍开始搜
    if(u * length == sum) return true;
    if(cur == length) return dfs(u+1, 0, 0);
    
    for(int i = start; i < n; i++){
        if(st[i] || cur + w[i] > length) continue;
        
        st[i] = true;
        if(dfs(u, cur+w[i], i+1)) return true;
        st[i] = false;
        
        if(!cur || cur + w[i] == length) return false; // 等效冗余:第一根棍or最后一根棍失败,剪枝。
        
        int j = i;  // 等长木棍略过
        while(j < n && w[j] == w[i]) j++;
        i = j-1; // =j-1是因为for循环还要让i+1
    }
    return false;
}

int main(){
    while(cin >> n, n){
        memset(st, 0, sizeof st);
        sum = 0;
        
        for(int i = 0; i < n; i ++){
            cin >> w[i];
            sum += w[i];   
        }
        
        sort(w, w+n);   // 优化搜索顺序,优先搜索长木棍(木棒>木棍)
        reverse(w, w+n);
        
        length = 1;
        while(true){
            if(sum % length == 0 && dfs(0, 0, 0)){  // 枚举木棒原始长度,木棒长度应为木棍总长度的约数
                cout << length << endl;
                break;
            }
            length ++;
        }
    }
    return 0;
}

迭代加深

双向DFS


IDA*


(2)BFS

基本BFS

// AcWing 844. 走迷宫
// 求距离
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

typedef pair<int, int> PII; // 坐标
const int N = 110;

int n, m;
int g[N][N], d[N][N];
// g迷宫;
// d标记状态,d[x][y]表示(x, y)到原点的距离

int bfs(){
    // bfs队列
    queue<PII> 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};
    // (-1, 0)(0, 1)(1, 0)(0, -1)表示⬅️⬆️➡️⬇️(dx和dy写反了
    
    while(q.size()){
        // 弹出队头
        auto t = q.front();
        q.pop();
        
        // 遍历邻近节点
        for(int i = 0; i < 4; i++){
            int x = t.first + dx[i], y = t.second + dy[i];
            // x、y合法 且 有路 且 没走过
            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(){
    scanf("%d%d", &n, &m);
    
    for(int i = 0; i < n; i ++)
        for(int j = 0; j < m; j++)
            scanf("%d", &g[i][j]);
            
    printf("%d", bfs());
    
    return 0;
}
// AcWing 847. 图中点的层次
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int N = 1e5+10;

int n, m;
int h[N], e[N], ne[N], idx;
int d[N];

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int bfs(){// 返回最短路径
    queue<int> q;
    q.push(1);
    
    memset(d, -1, sizeof d);
    d[1] = 0;
    
    while(q.size()){
        int t = q.front();
        q.pop();
        
        for(int i = h[t]; i != -1; i = ne[i]){
            int j = e[i];
            if(d[j] == -1){
                d[j] = d[t] + 1;
                q.push(j);
            }
        }
    }
    return d[n];
}

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);
    }
    
    cout << bfs() << endl;
    
    return 0;
}

Flood Fill

// AcWing 1097. 池塘计数
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

#define x first
#define y second
typedef pair<int, int> PII;

const int N = 1010, M = N * N;

int n, m;
char g[N][N];
bool st[N][N];
PII q[M];   // 数组模拟队列

void bfs(int sx, int sy){   // 从起点(sx, sy)八连通flood_fill
    int hh = 0, tt  = 0;
    q[0] = {sx, sy};
    st[sx][sy] = true;
    
    while(hh <= tt){
        PII t = q[hh++];    // 取下队头
        
        for(int i = t.x - 1; i <= t.x + 1; i ++)
            for(int j = t.y - 1; j <= t.y + 1; j++){ // 八连通
                if(i == t.x && j == t.y) continue;  // 起点
                if(i < 0 || i >= n || j < 0 || j >= m)  continue;   // 越界
                if(g[i][j] == '.' || st[i][j]) continue;    // 不含雨水||已经遍历过
                
                q[++tt] = {i, j};   // 入队
                st[i][j] = true;
            }
    }
}

int main(){
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++) scanf("%s", g[i]);   // 按行读入
            
    int cnt = 0;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            if(g[i][j] == 'W' && !st[i][j]){    //  含雨水&&没有遍历过,则开始flood_fill
                bfs(i, j);
                cnt ++;
            }
    
    printf("%d\n", cnt);
    return 0;
}

最短路模型

从一个点走到另一个点

// AcWing 1076. 迷宫问题
// 求最短路径
#include <iostream>
#include <cstring>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;

const int N = 1010, M = N*N;

int n;
int g[N][N];
PII q[M];
PII pre[N][N];  // pre[x][y] = t表示点(x,y)是从点t来的(存路径同时记录st)


void bfs(int sx, int sy){
    int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};
    
    int hh = 0, tt = 0;
    q[0] = {sx, sy};    //  找到的第一条路径就是最短路径,不用记录st
    
    memset(pre, -1, sizeof pre);
    pre[sx][sy] = {0, 0};
    while(hh <= tt){
        PII t = q[hh++]; 
        
        for(int i = 0; i < 4; i++){
            int a = t.x + dx[i], b = t.y + dy[i];
            if(a < 0 || a >= n || b < 0 || b >= n) continue;
            if(g[a][b]) continue;
            if(pre[a][b].x != -1) continue; // 走过了
            
            q[++tt] = {a, b};
            pre[a][b] = t;
        }
    }
}

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++)
            scanf("%d", &g[i][j]);
    
    bfs(n-1, n-1);  // 倒着搜
    
    PII end(0, 0);
    while(true){
        printf("%d %d\n", end.x, end.y);
        if(end.x == n-1 && end.y == n-1) break;
        end = pre[end.x][end.y];
    }
    
    return 0;
}

最小步数模型

矩阵从一个状态变成另一种状态

// AcWing 1107. 魔板
#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>
using namespace std;

char g[2][4];
unordered_map<string, pair<char, string>> pre;  //  状态,(操作,pre状态)
unordered_map<string, int> dist;    // 状态,距离

void set(string state){ 
    for(int i = 0; i < 4; i++) g[0][i] = state[i];
    for(int i = 7, j = 0; j < 4; i--, j++) g[1][j] = state[i];
}

string get(){
    string res;
    for(int i = 0; i < 4; i++) res += g[0][i];
    for(int i = 3; i >= 0; i--) res += g[1][i];
    return res;
}

string move0(string state){
    set(state);
    for(int i = 0; i < 4; i++) swap(g[0][i], g[1][i]);
    return get();
}

string move1(string state){
    set(state);
    int v0 = g[0][3], v1 = g[1][3];
    for(int i = 3; i > 0; i--){
        g[0][i] = g[0][i-1];
        g[1][i] = g[1][i-1];
    }
    g[0][0] = v0, g[1][0] = v1;
    return get();
}

string move2(string state){
    set(state);
    int v = g[0][1];
    g[0][1] = g[1][1];
    g[1][1] = g[1][2];
    g[1][2] = g[0][2];
    g[0][2] = v;
    return get();
}

int bfs(string start, string end){
    if(start == end) return 0;
    
    queue<string> q;
    q.push(start);
    dist[start] = 0;
    
    while(!q.empty()){
        auto t = q.front();
        q.pop();
        
        string m[3];
        m[0] = move0(t), m[1] = move1(t), m[2] = move2(t);
        for(int i = 0; i < 3; i++){
            if(!dist.count(m[i])){
                dist[m[i]] = dist[t] + 1;
                pre[m[i]] = {'A'+ i, t};
                q.push(m[i]);
                if(m[i] == end) return dist[end];
            }   
        }
    }
    return -1;
}

int main(){
    string start, end;
    for(int i = 0, x; i < 8; i++){
        cin >> x;
        end += char(x + '0');   // 数字转字符
    }
    for(int i = 1; i <= 8; i++) start += char(i + '0');
    
    int step = bfs(start, end);
    cout << step << endl;
    
    string res;
    while(end != start){
        res += pre[end].first;
        end = pre[end].second;
    }
    reverse(res.begin(), res.end());
    
    if(step > 0) cout << res << endl;
    
    return 0;
}

双端队列BFS

// AcWing 175. 电路维修
// 该图:点之间能走通可以看作边权为0,不相连看作边权为1;横纵坐标之和为奇数的点永远无法到达。
#include <iostream>
#include <deque>
#include <algorithm>
#include <cstring>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 510;
int r, c;
char g[N][N];
int dist[N][N];
bool st[N][N];

int bfs(){
    deque<PII> q;
    q.push_back({0, 0});
    
    memset(dist, 0x3f, sizeof dist);
    dist[0][0] = 0;
    memset(st, 0, sizeof st);
    
    int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1}; //  通过当前格点计算四个方向的格点坐标(逆时针)
    int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1}; //  通过当前格点计算边坐标(g[x][y],逆时针)
    char ch[] = "\\/\\/";  // \/\/,四个方向的边,逆时针
    while(q.size()){
        auto t = q.front();
        q.pop_front();
        
        int x = t.x, y = t.y;
        if(st[x][y]) continue;// (点第一次出队列时才是最优的,入队列不一定)
        st[x][y] = true;
        
        for(int i = 0; i < 4; i++){
            int a = x + dx[i], b = y + dy[i];   //  四个方向的格点坐标
            if(a < 0 || a > r || b < 0 || b > c) continue;
            
            int ga = x + ix[i], gb = y + iy[i]; // 四个方向边坐标
            int w = (g[ga][gb] != ch[i]);
            int d = dist[x][y] + w;
            if(d < dist[a][b]){ // 更新四个方向格点的距离
                dist[a][b] = d;
                if(!w) q.push_front({a, b});    // 边权为0(能走通),插入队头
                else q.push_back({a, b});   // 边权为1(不能走通),插入队尾
            }
        }
    }
    return dist[r][c];
}

int main(){
    int t; scanf("%d", &t);
    while(t--){
        scanf("%d%d", &r, &c);
        for(int i = 0; i < r; i++) scanf("%s", g[i]);
        
        int res = bfs();
        if(res == 0x3f3f3f3f)  puts("NO SOLUTION"); //  性质:下标之和为奇数的格点无法到达
        else printf("%d\n", res);
    }
    return 0;
}
// 汗流浃背了,好难
// P1948 [USACO08JAN] Telephone Lines S

优先队列BFS

双向BFS

// AcWing 190. 字串变换
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;

const int N = 6;
int n;
string A, B;
string a[N], b[N];

int extend(queue<string>& q, unordered_map<string, int>& da, unordered_map<string, int>& db, string a[N], string b[N]){
    // 扩展a
    int d = da[q.front()];
    while(q.size() && da[q.front()]==d){    // 和队头元素在同一层(每次扩展一层,而不是一个)
        auto t = q.front();
        q.pop();
        
        for(int i = 0; i < n; i++)  // 遍历所有的替换规则
            for(int j = 0; j < t.size(); j++)
                if(t.substr(j, a[i].size()) == a[i]){   // 如果t串中包含字符串a
                    string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size());   // 替换
                    if(db.count(r)) return da[t] + db[r] + 1;   
                    if(da.count(r)) continue;
                    da[r] = da[t] + 1;
                    q.push(r);
                }
    }
    return 11;
}

int bfs(){
    if(A == B) return 0;
    queue<string> qa, qb;
    unordered_map<string, int> da, db;  // dist
    
    qa.push(A), qb.push(B);
    da[A] =0, da[B] = 0;
    
    int step = 0;
    while(qa.size() && qb.size()){
        // 每次选择节点少的队列扩展,总共搜到的点数会更少
        int t = qa.size() < qb.size() ? extend(qa, da, db, a, b) : extend(qb, db, da, b, a);
        
        if(t <= 10) return t;
        if(++step == 10) return -1;
    }
    return -1;
}


int main(){
    cin >> A >> B;
    while(cin >> a[n] >> b[n]) n++;
    
    int res = bfs();
    if(res == -1) puts("NO ANSWER!");
    else cout << res << endl;
    
    return 0;
}

A*

一定有解的情况下才能使用

//  AcWing 179. 八数码
// 八数码问题:如果读出来的逆序对的数量是偶数,则一定有解!
#include <iostream>  
#include <algorithm>  
#include <queue>  
#include <unordered_map>  
using namespace std;  
  
typedef pair<int, string> PIS;  
  
int f(string state){// 估价函数,应使终点估计距离f(state) <= 终点真实距离  
    int res = 0;  
    for(int i = 0; i < state.size(); i++){  
        if(state[i] != 'x'){  
            int t = state[i] - '1';  
            res += abs(i/3 - t/3) + abs(i%3 - t%3); // 当前坐标(i/3, i%3) 真实坐标(t/3, t%3)  
        }  
    }  
    return res; // 曼哈顿距离之和  
}  
  
string bfs(string start){  
    string end = "12345678x";  
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};  
    char op[4] = {'u', 'r', 'd', 'l'};  
  
    unordered_map<string, int> dist;    // 状态,距离  
    unordered_map<string, pair<string, char>> prev; // 状态,(前一个状态,操作)  
    priority_queue<PIS, vector<PIS>, greater<PIS>> heap;    // 小根堆(起点距离+终点估计距离,状态)  
  
    dist[start] = 0;  
    heap.push({f(start), start});  
  
    while(heap.size()){  
        auto t = heap.top();    // 每次取出起点距离+估计距离最小的  
        heap.pop();  
  
        string state = t.second;  
        if(state == end) break; // 只保证终点第一次出队时的最短路径是最优解  
  
  
        int x, y;  
        for(int i = 0; i < state.size(); i++)   // 求x的坐标  
            if(state[i] == 'x'){  
                x = i / 3, y = i % 3;  
                break;            }  
  
        int step = dist[state];  
        string source = state;  // 记录source  
        for(int i = 0; i < 4; i++){  
            int a = x + dx[i], b = y + dy[i];  
            if(a >= 0 && a < 3 && b >= 0 && b < 3){  
                swap(state[x*3+y], state[a*3+b]);  
                if(!dist.count(state) || dist[state] > step + 1){   // 更新距离  
                    dist[state] = step + 1;  
                    prev[state] = {source, op[i]};  
                    heap.push({dist[state] + f(state), state});  
                }  
                swap(state[x*3+y], state[a*3+b]);  
            }  
        }  
    }  
  
    string res;  
    while(end != start){  
        res += prev[end].second;  
        end = prev[end].first;  
    }  
    reverse(res.begin(), res.end());  
    return res;  
}  
  
int main(){  
    string start, seq;  
    char c;  
    while(cin >> c){  
        start += c;  
        if(c != 'x') seq += c;  
    }  
  
    int cnt = 0; // 逆序对数量  
    for(int i = 0; i < seq.size(); i++)  
        for(int j = i + 1; j < seq.size(); j++)  
            if(seq[i] > seq[j]) cnt++;  
  
    if(cnt & 1) puts("unsolvable");  
    else cout << bfs(start) << endl;  
  
    return 0;  
}
// AcWing 178. 第K短路
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;

typedef pair<int, int> PII;
typedef pair<int, PII> PIII;

const int N = 1010, M = 20010;
int n, m, s, t, k; 
int h[N], rh[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

void add(int h[], int a, int b, int c){
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}


void dijkstra(){    // 反向dijkstra(对反向图跑dijkstra,获得终点t到任意点的最短路径长度,作为A*的估计距离)
    priority_queue<PII, vector<PII>, greater<PII>> heap;    // (dist, 下标)
    heap.push({0, t});
    
    memset(dist, 0x3f, sizeof dist);
    dist[t] = 0;
    
    while(heap.size()){
        auto tmp = heap.top();
        heap.pop();
        
        int ver = tmp.second;
        if(st[ver]) continue;
        st[ver] = true;
        
        for(int i = rh[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});
            }
        }
    }
}

int astar(){
    priority_queue<PIII, vector<PIII>, greater<PIII>> heap; // (估计距离, (已走距离, 下标))
    heap.push({dist[s], {0, s}});
    
    while(heap.size()){
        auto tmp = heap.top();
        heap.pop();
        
        int ver = tmp.second.second, distance = tmp.second.first;
        cnt[ver] ++;    // 某个点第k次弹出时,就是第k短路径
        if(cnt[t] == k) return distance;
        
        for(int i = h[ver]; i != -1; i = ne[i]){
            int j = e[i];
            if(cnt[j] < k) heap.push({distance + w[i] + dist[j], {distance + w[i], j}});    // 限定每个点的访问次数
        }
    }
    return -1;
}

int main(){
    memset(h, -1, sizeof h);
    memset(rh, -1, sizeof rh);
    
    cin >> n >> m;
    for(int i = 0, a, b, c; i < m; i++){
        cin >> a >> b >> c;
        add(h, a, b, c), add(rh, b, a, c);
    }
    cin >> s >> t >> k;
    if(s == t) k++; // 起点=终点时,存在一条长度为0的自环
    
    dijkstra();
    cout << astar() << endl;
    
    return 0;
}

估价函数

  • 只允许上下左右四个方向移动=>曼哈顿距离
  • 允许朝八个方向移动=>对角距离
  • 允许朝任意方向移动=>欧几里得距离
// 曼哈顿距离
dx + dy
// 对角距离(?
D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)
// 欧几里得距离
sqrt(dx * dx + dy * dy)

多源BFS

在这里插入图片描述

// AcWing 173. 矩阵距离
// 求01矩阵每个0到1的最短距离(曼哈顿距离=行距离+列距离,单源BFS情况下会超时)
#include <iostream>
#include <cstring>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;

const int  N = 1010, M = N * N;

int n, m;
char g[N][N];
PII q[M];
int dist[N][N];

void bfs(){
    memset(dist, -1, sizeof dist);
    int hh = 0, tt = -1;
    for(int i = 0; i < n; i++) // 找起点
        for(int j = 0; j < m; j++)
            if(g[i][j] == '1'){
                dist[i][j] = 0; // 起点到虚拟源点的距离为0
                q[++tt] = {i, j};
            }
            
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    while(hh <= tt){
        PII t = q[hh++];
        
        for(int i = 0; i < 4; i++){
            int a = t.x + dx[i], b = t.y + dy[i];
            if(a < 0 || a >= n || b < 0 || b >= m) continue;
            if(dist[a][b] != -1) continue;
            
            dist[a][b] = dist[t.x][t.y] + 1;
            q[++tt] = {a, b};
        }
    }
}


int main(){
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++) scanf("%s", g[i]);
    
    bfs();
    
    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++) printf("%d ", dist[i][j]);
        puts("");
    }
    
    return 0;
}

(3)最短路径

在这里插入图片描述

Dijkstra

朴素的Dijkstra

  • 稠密图用邻接矩阵存储
// AcWing 849. Dijkstra求最短路 I
#include <iostream>
#include <cstring>
using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
// 以下下标都从1开始
int g[N][N];// 图的邻接矩阵
int dist[N];// 各点到1号的最短距离
bool st[N];// 某点是否已确定最短路径

int dijkstra(){
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    // 需要进行n-1次的求最短路径
    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; // 未确定最短路径 并且 到源点的距离比dist[t]还小
                
        // 用新点更新最短距离
        for(int j = 1; j <= n; j++)
            dist[j] = min(dist[j], dist[t] + g[t][j]);
            
        // 更新标记    
        st[t] = true;
    }
    
    return dist[n] == INF ? -1 : dist[n];
}

int main(){
    cin >> n >> m;
    
    // 初始化邻接矩阵
    memset(g, 0x3f, sizeof g);
    
    while(m --){
        int a, b, c;
        cin >> a >> b >> c;
        
        g[a][b] = min(g[a][b], c);// 处理重边
    }
    
    cout << dijkstra() << endl;
    
    return 0;
}

堆优化的Dijkstra

  • 稀疏图用邻接表存储
// AcWing 850. Dijkstra求最短路 II
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

typedef pair<int, int> PII;// 距离,点

const int N = 1e6 + 10, INF = 0x3f3f3f3f;

int n, m;
int 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 dijkstra(){
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});
    
    while(heap.size()){
        auto t = heap.top();
        heap.pop();
        int ver = t.second;
        
        // 如果已经处理过该点,跳过。
        if(st[ver]) continue;
        // 如果没有,处理
        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});
            }
        }
        st[ver] = true;// 更新标记
    }
    
    return dist[n] == INF ? -1 : dist[n];
}

int main(){
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h); // 初始化邻接表
    
    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);// 添加重边也没关系,有堆做优化
    }
    
    printf("%d", dijkstra());
    return 0;
}

Bellman-Ford

/*
	有向图中存在负权边,则不一定存在最短路径。如果存在最短路径,则最短路径上肯定没有负环。
	假设存在n个点,如果点1到n的最短路径存在,则最短路径的长度等于n-1。
	如果存在负环,则第n次时还能进行松弛操作。
*/
for(int i = 0; i < n; i++)
	/*
		有向图中,如果所有边都满足三角形不等式dist[y]<=dist[x]+c,则dist数组就是所求最短路。
	*/
	for(int j = 0; j < m; j++){
		auto e = edges[j];
		dist[e.b] = min(dist[e.b], dist[e.a] + e.c);// 松弛操作
	}
// AcWing 853. 有边数限制的最短路
#include <iostream>
#include <cstring>
using namespace std;

const int N = 510, M = 10010;

int n, m, k;
int dist[N], last[N];

struct Edge{
    int a, b, c;
}edges[M];

void bellman_ford(){
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    // 走k步
    for(int i = 0; i < k; i++){
        memcpy(last, dist, sizeof dist);// 拷贝上一次迭代的结果,避免k步限制失效,发生串联
        // 遍历所有的边
        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;
    
    for(int i = 0; i < m; i++){
        int a, b, c;
        cin >> a >> b >> c;
        edges[i] = {a, b, c};
    }
    
    bellman_ford();
    
    /*
	    用大于INF/2判断是否存在最短路径,可能存在未更新的点同样被边更新的情况。
	    到不了的几个点之间存在负权边。使得到不了的点的距离一定范围内减小,小于了INF。这时可以使用小一点的INF/2来做判断,大于INF/2则认为不存在路径。
    */
    if(dist[n] > 0x3f3f3f3f/2) puts("impossible");
    else cout << dist[n];
    
    return 0;
}

SPFA

Shortest Path Fast Algorithm
队列优化的Bellman-Ford算法

  • 图中没有负环才能用SPFA
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int N = 1e5+10;

int n, m;
int h[N], e[N], w[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];
            /*
                bellman-ford是不管这条边是否满足不等式,都要进行处理。
                spfa通过队列、该点的出边、不满足不等式,进行扩展处理。避免对不需要扩展的节点的冗余扫描。
            */
            // 如果不满足不等式
            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();
    /*
	    spfa是基于拓扑序列的,不存在bellman-ford算法中未更新的点同样被边更新的情况。所以用==0x3f3f3f3f来判断是否存在最短路径。
    */
    if(t == 0x3f3f3f3f) puts("impossible");
    else cout << t;
    
    return 0;
}
// AcWing 852. spfa判断负环
#include <iostream>
#include <cstring>
#include <queue>
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];// cnt记录到1到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(){
    // 找负环无需初始化dist数组,因为dist数组一开始已经是0了,所以接下来只会更新小于0的边权,也就是负权边,存在负环的话就会不停的更新
    queue<int> q;
    // 第一个点可能没有负环,则把所有点放进去。找的最短路径不一定从起点1开始
    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;
                
                // 最短路径超过n条边
                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 puts("No");
    
    return 0;
}

Floyd

#include <iostream>
using namespace std;

const int N = 210, INF = 1e9;

int n, m, k;
int d[N][N];

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]); 
    /*
	    d[k, i, j]表示从i出发,只经过1~k点,到达j的最短路径。
	    dp。确定d[i][k],枚举找最小的d[k][j]。
	*/
}

int main(){
    cin >> n >> m >> k;
    
    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;
    
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        d[a][b] = min(d[a][b], c); // 处理重边
    }
    
    floyd();
    
    while(k--){
        int a, b;
        cin >> a >> b;
        
        int t = d[a][b];
        if(t > INF/2) puts("impossible");
        else cout << t << endl;
    }
    return 0;
}

(4)最小生成树

在这里插入图片描述

Prim

朴素的Prim

#include <iostream>
#include <cstring>
using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N];
int dist[N]; // 记录点到集合的距离
bool st[N];

int prim(){ // 返回最小生成树边权重之和
    memset(dist, 0x3f, sizeof dist);
    
    int res = 0;
    for(int i = 0; i < n; i++){ // 遍历n次,找n个点加入集合
        int t = -1;
        
        for(int j = 1; j <= n; j++) // 找到集合外距离集合最近的点
            if(!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;
                
        if(i && dist[t] == INF) return INF; // 不是第一个点 且 t到集合的距离是INF(说明当前集合是不连通的)
        
        if(i) res += dist[t]; // 不是第一个点,加入集合
        st[t] = true;
        
        for(int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]); // 更新所有点到集合的距离
    }
		/*
			注意先累加再更新,避免t身上有自环时,更新了dist[t]后,再累加时dist[t]值已经错误。
		*/
    return res;
}

int main(){
    cin >> n >> m;
    
    memset(g, 0x3f, sizeof g);
    
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = g[b][a] = min(g[a][b], c); // 无向图建立两条边;去重边
    }
    
    int t = prim();
    if(t == INF) puts("impossible");
    else cout << t << endl;
    
    return 0;
}

堆优化的Prim

Kruskal

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10, M = 2e5 + 10, INF = 0x3f3f3f3f;

int n, m;
int p[N]; // 并查集

struct Edge{
    int a, b, w;
    
    bool operator< (const Edge &e)const{
        return w < e.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); // 利用sort对所有的边进行排序
    
    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不在一个集合里,合并a、b
            p[a] = b;
            res += w;
            cnt ++;
        }
    }
    
    if(cnt < n-1) return INF; // 最小生成树的边有n-1条
    return res;
}

int main(){
    cin >> n >> m;
    
    for(int i = 0; i <= m; i++){
        int a, b, c;
        cin >> a >> b >> c;
        edges[i] = {a, b, c};
    }
    
    int t = kruskal();
    if(t == INF) puts("impossible");
    else cout << t << endl;
    
    return 0;
}

(5)二分图

在这里插入图片描述

染色法

#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5 + 10, M = N * 2;

int n, m;
int h[N], ne[M], e[M], idx;
int color[N];

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool dfs(int u, int c){ // 返回是否无冲突发生
    color[u] = c; // 从u开始,u染c种颜色
    
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        if(!color[j]){ // 如果j点没有染过颜色
            if(!dfs(j, 3 - c)) return false; // dfs染j点为c的异种颜色
        }
        else if(color[j] == c) return false;
    }
    return true;
}

int main(){
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while(m--){
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a); // 无向图
    }
    
    bool flag = true;
    for(int i = 1; i <= n; i++)
        if(!color[i]) // 如果i点没有染过颜色
            if(!dfs(i, 1)){ // 如果发生冲突
                flag = false;
                break;
            }
    
    if(flag) puts("Yes");
    else puts("No");
    
    return 0;
}

匈牙利算法

#include <iostream>
#include <cstring>
using namespace std;

const int N = 510, M = 1e5 + 10;

int n1, n2, m;
int h[N], e[M], ne[M], idx; // 左边点的邻接表
int match[N]; // 右边的点对应左边哪个点
bool st[N];

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool find(int x){
    for(int i = h[x]; i != -1; i = ne[i]){
        int j = e[i];
        if(!st[j]){
            st[j] = true; // 设为true防止find(match[j])无限递归
            if(match[j] == 0 || find(match[j])){ // 如果没有匹配过or对应匹配的点能找到下家
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

int main(){
    cin >> n1 >> n2 >> m;
    
    memset(h, -1, sizeof h);
    
    while(m--){
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    
    int res = 0;
    for(int i = 1; i <= n1; i++){
        memset(st, false, sizeof st); // 每次更新为false,否则除了第一次匹配的,后面的都无法挖墙脚达到最大匹配
        if(find(i)) res ++;
    }
    
    cout << res;
    
    return 0;
}

(6)欧拉回路和欧拉路径

(7)拓扑排序

// AcWing 848. 有向图的拓扑序列
#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N];// d存储每个点的入度

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 返回是否存在拓扑序列
bool topsort(){
    int hh = 0, tt = -1;
    
    // 入度为0的点入队
    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;// 入度为0的点入队
        }
    }
    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] ++;
    }
    
    if(!topsort()) puts("-1");
    else{
        for(int i = 0; i < n; i++) cout << q[i] << ' ';
    }
    
    return 0;
}

(8)网络流

四、数学知识

(1)质数

从2开始的整数开始定义。

// 判定是否为质数 - 试除法 O(sqrt(n))
bool is_prime(int n){
	if(n < 2) return false;
	for(int i = 2; i <= n / i; i++)
		if(n % i == 0) return false;
	return true;
}

// 求n的质因子 - 试除法 O(logn ~ sqrt(n))
void divide(int n){
	for(int i = 2; i <= n / i; i++)
		if(n % i == 0){
			int s = 0;
			while(n % i == 0){
				 n /= i;
				 s++;
			}
			cout << i << " " << s << endl; // i^s
		}
	if(n > 1) cout << n << " " << 1 << endl; // n中最多只包含一个>sqr t(n)的质因子
}

// 求n以内的质数及个数 - 埃氏筛法 O(nloglogn)
bool st[N]; // 标记是否筛过
int primes[N], cnt = 0; // n以内的质数,质数个数
void get_primes(int n){
	for(int i = 2; i <= n; i++)
		if(!st[i]){
			primes[cnt ++] = i; 
			for(int j = i + i; j <= n; j += i) st[j] = true; // 质数的倍数是合数,把st[质数的倍数]=true
		}
}

// 求n以内的质数及个数 - 线性筛法 
bool st[N];
int primes[N], cnt = 0;
void get_primes(int n){
	for(int i = 2; i <= n; i++){
		if(!st[i]) primes[cnt ++] = i;
		for(int j = 0; primes[j] <= n / i; j++){ // 从小到大枚举所有的质数 
			st[primes[j] * i] = true;
			if(i % primes[j] == 0) break; 
			// 当i%primes[j]==0时,primes[j]是i的最小质因子,则primes[j]也是primes[j]*i的最小质因子;把primes[j]*i筛掉
			// 当ii%primes[j]!=0时,primes[j]<i的最小质因子,则primes[j]也是primes[j]*i的最小质因子
			// 合数一定存在一个最小质因子,让合数只会被最小质因子筛掉,且只被筛一次
		}
	}
}

(2)约数

// 求n的所有约数 - 试除法
vector<int> get_divisors(int n){
	vector<int> res;
	for(int i = 1; i <= n/i; i++){
		if(n % i == 0){
			res.push_back(i);
			if(i != n/i) res.push_back(n/i); // 约数i和n/i成对出现
		}
	}
	sort(res.begin(), res.end());
	return res;
}

// 求n的约数个数、约数和 - 试除法
// 分解质因数n=p1^a1 * p2^a2 * ... * pn^an
// 约数的个数=(a1+1)(a2+1)...(an+1)
unordered_map<int, int> primes;
while(n--){
	cin >> a;
	for(int i = 2; i <= a/i; i++){
		while(a % i == 0){
			a /= i;
			primes[i] ++;
		}
	}
	if(a > 1) primes[a] ++;
}
// 约数的和=(p1^0 + p1^1 + ... + p1^a1)...(pn^0 + pn^1 + ... + pn^an)
for(auto prime : primes){
        int p = prime.first, a = prime.second;
        LL t = 1;
        while(a --) t = (t * p + 1) % mod; // 秦九韶算法,计算pn^0 + pn^1 + ... + pn^an
        res = res * t % mod;
    }

// 求a和b的最大公约数 - 欧几里得算法(辗转相除法)
// a%d=0且b%d=0,则(ax+by)%d=0;gcd(a,b)=gcd(b,a%b)=gcd(b,a-cb)
int gcd(int a, int b){
	return b ? gcb(b, a % b) : a; // a和0的最大公约数是a
}

(3)欧拉函数

(4)快速幂


(5)扩展欧几里得算法

(6)中国剩余定理

(7)高斯消元

(8)组合计数

(9)容斥原理

(10)简单博弈论

五、动态规划

(1)背包问题

01背包

每个物品只有一个,最多选一次

// 二维
#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v, w;
int f[N][N]; // f[i][j]表示 只选前i个物品 且 总体积<=j 的最大总价值

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++){ // f[0][?]都为0,所以i从1开始
	    cin >> v >> w;
        for(int j = 0; j <= m; j++){
            f[i][j] = f[i - 1][j]; // 不含第i个物品的情况
            if(j >= v) f[i][j] = max(f[i][j], f[i - 1][j - v] + w); // 如果含第i个物品的情况存在的话,比较不含、含第i个物品的情况  
        }
    }
    
    cout << f[n][m];
    
    return 0;
}

// 一维优化
// 利用滚动数组
#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v, w;
int f[N]; // 总体积<=j的最大总价值

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++){
	    cin >> v >> w;
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }
            
    cout << f[m] << endl;
    return 0;
}

完全背包

每个物品有无限个,可以选无限次

// 三维
#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v, w;
int f[N][N];

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++){
        cin >> v >> w;
        for(int j = 0; j <= m; j ++)
            for(int k = 0; k * v <= j; k ++) // 选k个第i个物品
                f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w); // 比较f[i-1][j-k*v]+k*w中的最大值
    }
    
    cout << f[n][m];
    return 0;
}

// 二维优化
#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v, w;
int f[N][N];

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++){
        cin >> v >> w;
        for(int j = 0; j <= m; j ++){
	        f[i][j] = f[i - 1][j];
	        if(j >= v) f[i][j] = max(f[i][j], f[i][j - v] + w);
        }
    }
    
    cout << f[n][m];
    return 0;
}

// 一维优化
#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v, w;
int f[N];

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++){
        cin >> v >> w;
        for(int j = v; j <= m; j ++)
            f[j] = max(f[j], f[j - v] + w);
    }
    
    cout << f[m];
    return 0;
}

多重背包

每个物品有Si个,最多选Si次

#include <iostream>
using namespace std;

const int N = 110;

int n, m;
int v, w, s;
int f[N][N];

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++){
        cin >> v >> w >> s;
        for(int j = 0; j <= m; j ++)
            for(int k = 0; k <= s && k * v <= j; k ++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
    }
        
    cout << f[n][m];
    return 0;
}

// 二进制优化
#include <iostream>
using namespace std;

const int N = 12010, M = 2010; // N物品数=1000*log2000+10;M体积

int n, m;
int v[N], w[N];
int f[M];

int main(){
    cin >> n >> m;
    
    int cnt = 0; // 标记打包下标
    for(int i = 1; i <= n; i ++){
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1; // 从每个包里装1个物品开始打包
        while(k <= s){ // 每次把k个物品装成一个01背包
            v[++cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if(s > 0){ // 还剩c=s个没装
            v[++cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    
    n = cnt;
    
    for(int i = 1; i <= n; i++)
        for(int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j-v[i]]+w[i]);
    
    cout << f[m];
    
    return 0;
}

分组背包

每组物品有Si个,最多选一个

#include <iostream>
using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N]; 

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++){
        cin >> s[i];
        for(int j = 0; j < s[i]; j ++)
            cin >> v[i][j] >> w[i][j];
    }
            
    for(int i = 1; i <= n; i ++)
        for(int j = m; j >= 0; j --)
            for(int k = 0; k < s[i]; k++)
                if(j >= v[i][k]) // 不能写在j循环里,因为v[i][k]随着k改变而动态变化
                    f[j] = max(f[j], f[j-v[i][k]]+w[i][k]);
                    
    cout << f[m];
    return 0;
}

(2)线性DP

// AcWing 898. 数字三角形
#include <iostream>
using namespace std;

const int N = 510, INF = 1e9;

int n;
int a[N][N];
int f[N][N];

int main(){
    cin >> n;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= i; j++)
            cin >> a[i][j];
            
    for(int i = 0; i <= n; i ++)
        for(int j = 0; j <= i + 1; j ++) // 边界的右上角也要初始化
            f[i][j] = -INF; // 初始化状态
            
    f[1][1] = a[1][1];
    for(int i = 2; i <= n; i ++)
        for(int j = 1; j <= i; j++)
            f[i][j] = max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]); // 来自左上角or来自右上角
    
    int res = -INF;
    for(int i = 1; i <= n; i++) res = max(res, f[n][i]); // 遍历最后一行找最大路径
    
    printf("%d", res);
    return 0;
}
// AcWing 895. 最长上升子序列
#include <iostream>
using namespace std;

const int N = 1010;

int n;
int a[N], f[N]; // 以a[i]结尾的子序列长度

int main(){
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    for(int i = 1; i <= n; i++){
        f[i] = 1; // 只有a[i]一个数
        for(int j = 1; j < i; j++) // 从a[1]~a[i-1]找<a[i]的数
            if(a[j] < a[i])
                f[i] = max(f[i], f[j] + 1);
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++) res = max(res, f[i]);
    
    cout << res;
    return 0;
}
// AcWing 897. 最长公共子序列
#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N]; // 所有由第一个序列的前i个字母、第二个序列的前j个字母构成的公共子序列个数

int main(){
    scanf("%d%d", &n, &m);
    scanf("%s%s", a+1, b+1);
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            f[i][j] = max(f[i-1][j], f[i][j-1]); // f[i-1][j]和f[i][j-1],它们都包含f[i-1][j-1]的情况
            if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
        }
        
    printf("%d", f[n][m]);
    return 0;
}

(3)区间DP

// AcWing 282. 石子合并
#include <iostream>
using namespace std;

const int N = 310, INF = 1e8;

int n;
int s[N];
int f[N][N];

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &s[i]);
    
    for(int i = 1; i <= n; i++) s[i] += s[i-1]; // 计算前缀和
    
    for(int len = 2; len <= n; len++) // 枚举区间长度
        for(int l = 1; l + len - 1 <= n; l++){  // 枚举左端点
            int r = l + len -1;
            f[l][r] = INF;
            for(int k = l; k < r; k++) // 枚举分界点k
                f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]);
        }
        
    printf("%d", f[1][n]);
    return 0;
}

(4)计数类DP




(5)数位统计DP

// AcWing 338. 计数问题


(6)状态压缩DP


(7)树形DP


(8)记忆化搜索


六、贪心

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值