算法笔记(Acwing)

算法笔记

数据结构与算法

排序

快速排序

l到r一段数据排序

  1. 确定分界点:q[l],q[(l+r)/2],q[r],随机
  2. 划分区间:令左区间的值都<=x,右区间的值都>=x.
  3. 递归排序左右两段。

主要思想:采用双指针来划分区间,头尾两个指针,i指针发现指向的值小于x继续向后走,大于x则停止,等j指针大于x继续向前走,走到小于x时,交换i和j指向的值,然后继续,直到两指针相遇。

void quick_sort(int q[],int l,int r)
{
    if(l >= r) return;
    int x = q[r],i = l - 1,j = r + 1;//确定分界点,双指针
    while(i < j)
    {
        do i ++;while (q[i]<x);//i指针移动
        do j --;while (q[j]>x);//j指针移动
        if(i < j) swap(q[i],q[j]);
    }
    quick_sort(q,l,j);
    quick_sort(q,j + 1,r);
}

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

归并排序nlogn

  1. 以中间值为分界点,mid = (l + r)/2
  2. 递归排序左右两边
  3. 归并,合二为一

第三步归并思想:左右两数组的头指针,比较两值,较小的放在新的数组里,然后指针向后走,直到有一个数组走完为止,然后再把另一个数组剩下的数放到新数组里。最后再把新数组复制给q[].

int q[N],tmp[N];
void merge_sort(int q[],int l,int r)
{
    int mid = l + r >>1;
    merge_sort(q,l,mid),merge_sort(q,mid+1,r);//递归排序左右两边
    int k = 0,i = l,j = mid+1;
    while(i <= mid && j<=r)//归并
    {
        if(q[i]<q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    }
    while(i<=mid)tmp[k++] = q[i++];//有剩余数
    while(j<=r) tmp[k++] = q[j++];
    for(int i = l,j = 0;i<=r;i++,j++) q[i] = tmp[j];
}

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

二分

整数二分

模板

先确定边界,再确定mid = l+r>>1,再写check函数,如果后面是l = mid,那么mid还需要+1;

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

主要思想:二分总区间,每次选择答案所在的区间进行下一步的处理

acwing例题

浮点数二分

bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

高精度算法

大整数相加减,大整数×÷小整数

加法

  1. 大整数用数组存储
  2. 模拟人工加法
#include<iostream>
#include<vector>

using namespace std;
const int N = 1e6+10;

vector<int> add(vector<int> &A,vector<int> &B)//加引用提高效率,否则会copy数组
{
    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;
}

int main()
{
    string a,b;
    vector<int> A,B;
    cin>>a>>b;
    for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    for(int i = b.size()-1;i>=0;i--) B.push_back(b[i]-'0');//逆序读入两个大整数到数组里

    auto C= add(A,B);
    for(int i = C.size()-1;i>=0;i--)printf("%d",C[i]);
    return 0;
}

减法

#include<iostream>
#include<vector>

using namespace std;
const int N = 1e6+10;
//判断A是否大于B
bool cmp(vector<int> &A,vector<int> &B)
{
    if(A.size!=B.size()) return A.size() > B.size();
    else
    {
        for(int i = A.size()-1;i>=0;i--)
        {
            if (A[i]!=B[i]) return A[i]>B[i];
        }
        return true;
    }
}

vector<int> sub(vector<int> &A,vector<int> &B)//加引用提高效率,否则会copy数组
{
    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>0就输出t,t<0就输出t+10
        if(t<0) t=1;
        else t = 0;
    }
    while(C.size()>1&&C.back()==0)C.pop_back();//删除前导0
    return C;
}

int main()
{
    string a,b;
    vector<int> A,B;
    cin>>a>>b;
    for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    for(int i = b.size()-1;i>=0;i--) B.push_back(b[i]-'0');//逆序读入两个大整数到数组里
    if(cmp(A,B))
    {
        auto C = sub(A,B);
        for(int i = C.size()-1;i>=0;i--)printf("%d",C[i]);
    }
    else
    {
        auto C = sub(B,A);
        printf("-");
        for(int i = C.size()-1;i>=0;i--)printf("%d",C[i]);
    }
    return 0;
}

乘法

vector<int> mul(vector<int> &A, int b)
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}

除法

// A / b = C ... r, A >= 0, b > 0,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;
}

前缀和算法

S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]

  1. 如何求Si
S[0] = 0;
for(i=1;i<=n;i++)
{
    S[i] = S[i-1] + a[i];
}

2.作用:求数组中一段数[l,r]的和。通过S[r]-S[l-1]。

#include <iostream>

using namespace std;
const int N = 100010;

int n,m;
int a[N],s[N];

int main()
{
    scanf("%d%d", &n,&m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &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]);
    }
}

二维前缀和

原数组aij

Sij表示aij左上角所有元素的和

如何求Sij=Si-1,j+Si,j-1-Si-1,j-1+aij

某一个矩形内的数之和如何求?以(x1,y1)和(x2,y2)为左上角和右下角的矩形

代码:

#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]);

    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[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);//计算某一段和
    }
    return 0;
}

差分(前缀和的逆运算)

原数组a1,a2,a3...

构造b1,b2,b3...使得:ai = b1+b2+...+bi

可以通过求B的前缀和来得到A数组

用处:让a数组的某一个区间内都加上C时使用差分,循环加的话复杂度是O(n)

当bl+c时,al,al+1一直到an都会加上c。

所以,让bl+C,br+1-C,就可以使a数组[l,r]范围内都加上C,时间复杂度为O(1);

一维差分:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;

int n,m;
int a[N],b[N];

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]);
    for (int i = 1; i <= n; i ++ ) insert(i,i,a[i]);//差分数组的初始化
    //for (int i = 1; i <= n; i ++ ) b[i]=a[i]-a[i-1];
    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]);
}


二维矩阵

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1001;
int a[N][N],b[N][N];

int n,m,q;

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;
        cin >> 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;
}

双指针算法

核心思想:

for(int i = 0;i < n;i++)

for(int j = 0;j < n;j++)

时间复杂度为O(n^2)

双指针算法可以将此算法优化到O(n)

for(i = 0,j = 0;i<n;i++)
{
    while(j<i&&check(i,j)) j++;
    //每道题目的具体逻辑
}

例题:将用空格分开的字符串逐个输出

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    string str;
    getline(cin,str);

    int n = str.size();

    for(int i = 0;i<n;i++)
    {
        int j = i;
        while(j<n&&str[j]!=' ') j++;
        //这道题的具体逻辑
        for(int k=i;k<j;k++) cout<<str[k];

        cout<<endl;
        i=j;//跳出这一段
    }
    return 0;
}

位运算

求n的二进制表示中第k位是几

n >> k & 1

k是从个位往大数的第几位

lowbit(x) = x & -x

返回x的最后一位1(如x为101000,lowbit(x)=1000)

例题:求一个数中1的个数

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

int main()
{
    int x;
    cin >> x;
    int res = 0;
    while(x) x -= lowbit(x),res++;//每次减去x的最后一位1.
    cout << res << endl;
}

离散化

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

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于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, 2, ...n
}


区间合并

将所有存在交集的区间合并

做法:

  1. 按区间左断端点排序
  2. 扫描后面的区间,如果下一个区间的起点大于当前区间的终点,则更新起止位置,否则将区间右端点更新成原右端点和下一个区间终点的最大值。
void merge(vector<PII> &segs)
{
    vector<PII> res;
    sort(segs.begin(), segs.end());

    int st = -2e9, ed = -2e9;
    for (auto seg : segs)
    if (ed < seg.first)
    {
        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;
}

链表

由于用结构体实现链表非常慢,所以这里采用数组实现链表

单链表->邻接表->存储图和树

用e[N]来实现链表val,用ne[N]来实现指针;

比如此链表:

e[0] = 3, e[1] = 5,e[2] = 7, e[3]=9;

ne[0]=1,ne[1]=2,ne[2]=3,ne[3]=-1

代码实现:

#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,idx++;
}

//将x插入到下标是k的点的后面
void add(int k,int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}
//将下标是k的点后面的点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];//ne[ne[k]]表示k往后两次指向的值
}

双链表

用l[N]来表示左边的点,r[N]表示右边的点,e[N]表示每个点的值

0:head,1:tail

#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;
}
//在下标是k的点右边插入x
void add(int k,int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;
}
//在k左边插入可以直接调用add(l[k],x)!

//删除下标是k的点
void remove(k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}


栈与队列

栈:先进后出

#include <iostream>

using namespace std;

const int N = 100010;

int stk[N],tt;

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

//弹出
tt--;

//判断栈是否为空
if(tt>0) not empty
else empty;

//栈顶
stk[tt];

队列:先进先出

#include <iostream>

using namespace std;

const int N = 100010;

//在队尾tt插入元素,在队头hh弹出元素
int q[N],hh,tt = -1;

//insert
q[ ++ tt] = x;

//弹出
hh ++;

//判断是否为空
if(hh<=tt) not empty
else empty;

//取出队头元素
q[hh]

单调栈

给定一个序列,找到每一个数左边离它最近的且比它小的数是什么

例题

#include <iostream>
using namespace std;
const int N = 100010;

int n;
int stk[N],tt;

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        while(tt && stk[tt]>=x) tt--;
        if(tt) cout << stk[tt]<<' ';
        else cout << "-1"<<' ';
        stk[++tt] = x;
    }
    return 0;
}

单调队列:滑动窗口

求滑动窗口里的最大值和最小值

例题

#include <iostream>
using namespace std;
const int N = 1000010;

int a[N],q[N];//队列q[N]存储的是原数组的下标
int n,k;

int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);

    int hh = 0,tt = -1;//队列初始化
    for (int i = 0; i < n; i ++ )
    {
        //判断队头是否已经滑出窗口
        if(hh <= tt && i-k+1 > q[hh]) hh++;//i-k+1是窗口长度
        //当队尾元素比即将入队的元素大时,就删除队尾,保证队列存放的永远是最小值
        while(hh <= tt && a[q[tt]] >= a[i]) tt--;
        q[ ++ tt] = i;
        if(i >= k - 1) printf("%d ",a[q[hh]]);//队头存放的永远是最小值
    }
    puts("");

    hh = 0,tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        //判断队头是否已经滑出窗口
        if(hh <= tt && i-k + 1 > q[hh]) hh++;//i-k+1是窗口长度
        //当队尾元素比即将入队的元素小时,就删除队尾,保证队列存放的永远是最大值
        while(hh <= tt && a[q[tt]] <= a[i]) tt--;
        q[ ++ tt] = i;
        if(i >= k-1) printf("%d ",a[q[hh]]);//队头存放的永远是最大值
    }
    return 0;
}


KMP

可见代码随想录中详解

代码随想录

例题

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) {
            return 0;
        }
        int next[needle.size()];
        getNext(next, needle);
        int j = 0;
        for (int i = 0; i < haystack.size(); i++) {
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            if (haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};


Trie树

用处:用来高效的存储和查找字符串集合的数据结构

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++ ;
}

// 查询字符串出现的次数
int query(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

并查集

作用:

1.将两个集合合并

2.询问两个元素是否在一个集合当中

基本原

每一个集合用一颗树来维护

每一个集合的编号就是根节点的编号

每一个点存储它的父节点,p[x]表示x的父节点

问题一:如何判断树根?

=> if(p[x] == x)

问题二:如何求x的集合编号:

=> while(p[x] != x) x = p[x]

问题三:如何合并两个集合

=> p[x]是x的集合编号,p[y]是y的集合编号。p[x] = y就可以实现合并集合,也就是将一个集合当成另一个集合的一个子树。

路径压缩优化:

在x第一次找到根节点之后,这条树上左右的点都直接指向根节点

(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);

(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);

(3)维护到祖宗节点距离的并查集:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

堆排序

如何手写一个堆:

1.插入一个数 heap[++size] = x;up(size);

2.求集合中的最小值 heap[1];

3.删除最小值 heap[1] = heap[size];size--;down(1)

4.删除任意元素 heap[k] = heap[size];size--;down(k);up(k);

5.修改任意元素 heap[k] = x;down(k);up(k);

堆是一颗完全二叉树

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n,m;
int h[N],size1;//heap,size1为当前heap中有多少元素

void down(int u)
{
    int t = u;
    if(u*2 <= size1 && h[u*2] < h[t]) t = u*2;//t有左儿子
    if(u*2 + 1 <= size1 && h[u*2 + 1] < h[t]) t = u*2 + 1;//t有右儿子
    if(u != t)
    {
        swap(h[u],h[t]);
        down(t);
    }
}

void up(int u)//上浮(把小数上浮)
{
    while(u/2 && h[u/2] > h[u])
    {
        swap(h[u/2],h[u]);
        u /= 2;
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    size1 = n;
    for (int i = n/2; i ; i -- ) down(i);//构建二叉堆

    while (m -- )
    {
        printf("%d ",h[1]);
        h[1] = h[size1];//删除堆顶(最小值)
        size1--;
        down(1);
    }

    return 0;
}

哈希表

  1. 存储结构--包括开放寻址法和拉链法
  2. 字符串哈希方式

哈希函数常用取模操作

如把-109~109的数映射到0~10^5上,可以hash(x) = x mod 10^5

常把取模的数取成质数,这样发生哈希冲突的概率最小

拉链法

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100003;

int h[N];//哈希一维数组
int e[N],ne[N],idx;//拉链法的链表

void insert(int x)
{
    int k = (x % N + N) % N;//+N再%N是为了使余数为正数。k就是哈希值
    e[idx] = x,ne[idx] = h[k],h[k] = idx++;//插入x到h[k]的位置
}

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()
{
    int n;
    scanf("%d", &n);

    memset(h, -1, sizeof h);//清空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;
}

开放寻址法(蹲坑法)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200003,null = 0x3f3f3f3f;

int h[N];

int find(int x)
{
    int k = (x % N + N) % N;
    while(h[k] != null && h[k] != x)
    {
        k++;
        if(k == N) k = 0;
    }
    return k;
}

int main()
{
    int n;
    scanf("%d", &n);

    memset(h, 0x3f, sizeof h);//清空h
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d",op,&x);
        int k = find(x);
        if(*op == 'I')
        {
            h[k] = x;
        }
        else
        {
            if(h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

字符串哈希

字符串前缀哈希法

全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1XnX1X2X3⋯Xn−1Xn 的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。

映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ(X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ
注意点:

  1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
  2. 冲突问题:通过巧妙设置P (131 或 13331) , Q (2^64)的值,一般可以理解为不产生冲突。

问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。
求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。

前缀和公式 h[i+1]=h[i]×P+str[i],i∈[0,n−1] ,h为前缀和数组,str为字符串数组区间和公式 h[l,r]=h[r]−h[l−1]×P^(r−l+1)
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P^2把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。

//可以用unsigned long long 定义字符串数组,这样就不用对Q取模,因为到2的64次方就溢出了
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef unsigned long long ULL;//typedef给一种变量类型起别名

const int N = 100010,P = 131;

int n,m;
char str[N];
ULL h[N],p[N];//h为前缀和字符串哈希数组,p为p进制数组

ULL get(int l, int r)  // 计算子串 str[l ~ r] 的哈希值
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    scanf("%d%d%s",&n,&m,str + 1);
    p[0] = 1;
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = p[i - 1] * P;
        h[i] = h[i - 1] * P + str[i];
    }
    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;
}

set作为哈希表

数组也是一种哈希表,但数组的大小是受限的!

当题目没有限制数值的大小,就无法使用数组来做哈希表了。

主要因为如下两点:

  • 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
  • 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

所以此时一样的做映射的话,就可以使用set了。

关于set,C++ 给提供了如下三种可用的数据结构:(详情请看关于哈希表,你该了解这些! (opens new window)

  • std::set
  • std::multiset
  • std::unordered_set

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希, 使用unordered_set 读写效率是最高的,本题并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。

202.快乐数 (opens new window)中,我们再次使用了unordered_set来判断一个数是否重复出现过。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

Snipaste_2022-08-11_19-06-54.png

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

map作为哈希表

来说一说:使用数组和set来做哈希法的局限。

  • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
  • set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。

map是一种<key, value>的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。

C++提供如下三种map::(详情请看关于哈希表,你该了解这些! (opens new window)

  • std::map
  • std::multimap
  • std::unordered_map

std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红黑树。

同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),1.两数之和 (opens new window)中并不需要key有序,选择std::unordered_map 效率更高!

std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

Snipaste_2022-08-11_19-07-05.png

深度优先搜索DFS

暴力搜索,全排列例题

#include<iostream>
using namespace std;
const int N = 10;
int path[N];//保存序列
bool state[N];//数字是否被用过
int n;
void dfs(int u)
{
    if(u == n)//数字填完了,输出
    {
        for(int i = 0; i < n; i++)//输出方案
            cout << path[i] << " ";
        cout << endl;
    }

    for(int i = 1; i <= n; i++)//空位上可以选择的数字为:1 ~ n
    {
        if(!state[i])//如果数字 i 没有被用过
        {
            path[u] = i;//放入空位
            state[i] = true;//数字被用,修改状态
            dfs(u + 1);//填下一个位
            state[i] = false;//回溯,取出 i
        }
    }
}

int main()
{
    cin >> n;
    dfs(0);
}

n-皇后问题

全排列法

代码分析

对角线 dg[u+i],反对角线udg[n−u+i]中的下标 u+i和 n−u+i表示的是截距

下面分析中的(x,y)相当于上面的(u,i)
反对角线 y=x+b, 截距 b=y−x,因为我们要把 b当做数组下标来用,显然 b 不能是负的,所以我们加上 +n (实际上+n+4,+2n都行),来保证是结果是正的,即 y - x + n
而对角线 y=−x+b, 截距是 b=y+x,这里截距一定是正的,所以不需要加偏移量

#include <iostream>
using namespace std;
const int N = 20;

// bool数组用来判断搜索的下一个位置是否可行
// col列,dg对角线,udg反对角线
// g[N][N]用来存路径

int n;
char g[N][N];
bool col[N], dg[N], udg[N];

void dfs(int u) {
    // u == n 表示已经搜了n行,故输出这条路径
    if (u == n) {
        for (int i = 0; i < n; i ++ ) puts(g[i]);   // 等价于cout << g[i] << endl;
        puts("");  // 换行
        return;
    }

    //对n个位置按行搜索
    for (int i = 0; i < n; i ++ )
        // 剪枝(对于不满足要求的点,不再继续往下搜索)
        // udg[n - u + i],+n是为了保证下标非负
        if (!col[i] && !dg[u + i] && !udg[n - u + i]) {
            g[u][i] = 'Q';
            col[i] = dg[u + i] = udg[n - u + i] = true;
            dfs(u + 1);
            col[i] = dg[u + i] = udg[n - u + i] = false; // 恢复现场 这步很关键
            g[u][i] = '.';
        }
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';

    dfs(0);

    return 0;
}

DFS按每个元素枚举

// 不同搜索顺序 时间复杂度不同  所以搜索顺序很重要!
#include <iostream>
using namespace std;
const int N = 20;

int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N]; // 因为是一个个搜索,所以加了row

// s表示已经放上去的皇后个数
void dfs(int x, int y, int s)
{
    // 处理超出边界的情况
    if (y == n) y = 0, x ++ ;

    if (x == n) { // x==n说明已经枚举完n^2个位置了
        if (s == n) { // s==n说明成功放上去了n个皇后
            for (int i = 0; i < n; i ++ ) puts(g[i]);
            puts("");
        }
        return;
    }

    // 分支1:放皇后
    if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) {
        g[x][y] = 'Q';
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
        dfs(x, y + 1, s + 1);
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
        g[x][y] = '.';
    }

    // 分支2:不放皇后
    dfs(x, y + 1, s);
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';

    dfs(0, 0, 0);

    return 0;
}

宽度优先搜索BFS

例题:

活动 - AcWing

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;

const int N = 1e2 + 7;
int g[N][N], d[N][N];//g数组存储的是地图,d数组存的是每个点到起点的距离。
int n, m;
PII q[N * N];//手写队列

int bfs()
{
    int hh = 0, tt = 0;
    q[0] = {0, 0};
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    
    memset(d, - 1, sizeof d);//距离初始化为- 1表示没有走过

    d[0][0] = 0;//表示起点走过了
    
    while(hh <= tt)
    {
        auto t = q[hh++];
        for (int i = 0; i < 4; i ++ )
        {
            int x = t.first+dx[i],y = t.second+dy[i];//x表示沿着此方向走会走到哪个点
            if(x >= 0 && y >= 0 && x < n && y < m && g[x][y] == 0 && d[x][y] == -1)
            {
                d[x][y] = d[t.first][t.second] + 1;
                q[ ++ tt ] = {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() << endl;
    return 0;
}

例题:八数码

#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
#include <queue>

using namespace std;

int bfs(string start)
{
    string end = "12345678x";
    queue<string> q;//队列
    unordered_map<string,int> d;//距离
    q.push(start);
    d[start] = 0;
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    
    while(q.size())
    {
        auto t = q.front();
        q.pop();
        int distance = d[t];
        
        if(t == end) return distance;
        //状态转移
        int k = t.find('x');//返回x的下标
        int x = k / 3,y = k % 3;//将一维数组的下标转化成二维数组的坐标
        
        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(t[k],t[a * 3 + b]);//a * 3 + b将二维数组坐标转化成一维下标
                
                if(!d.count(t))
                {
                    d[t] = distance + 1;
                    q.push(t);
                }
                
                swap(t[k],t[a * 3 + b]);
            }
        }
    }
    return -1;
}

int main()
{
    string start;
    for (int i = 0; i < 9; i ++ )
    {
        char c;
        cin >> c;
        start += c;
    }
    cout << bfs(start) << endl;
    return 0;
}

树与图的存储

树和图的存储基本都是用图的邻接表来做的

int h[N], e[M], ne[M], idx;

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

int main()
{
    memset(h, -1, sizeof h);
}

树与图的深度优先遍历

void dfs(int u)
{
    st[u] = true;//标记已经被搜过
    for (int i = h[u]; i != -1; i = ne[i] )
    {
        int j = e[i];
        if(!st[j]) dfs(j);
    }
}

例题:

AcWing 846. 树的重心 - AcWing

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边

int h[N]; //邻接表存储树,有n个节点,所以需要n个队列头节点
int e[M]; //存储元素
int ne[M]; //存储列表的next值
int idx; //单链表指针
int n; //题目所给的输入,n个节点
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目

bool st[N]; //记录节点是否被访问过,访问过则标记为true

//a所对应的单链表中插入b  a作为根 
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// dfs 框架
/*
void dfs(int u){
    st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        if(!st[j]) {
            dfs(j);
        }
    }
}
*/

//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) {
    int res = 0; //存储 删掉某个节点之后,最大的连通子图节点数
    st[u] = true; //标记访问过u节点
    int sum = 1; //存储 以u为根的树 的节点数, 包括u,如图中的4号节点

    //访问u的每个子节点
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        //因为每个节点的编号都是不一样的,所以 用编号为下标 来标记是否被访问过
        if (!st[j]) {
            int s = dfs(j);  // u节点的单棵子树节点数 如图中的size值
            res = max(res, s); // 记录最大联通子图的节点数
            sum += s; //以j为根的树 的节点数
        }
    }

    //n-sum 如图中的n-size值,不包括根节点4;
    res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
    ans = min(res, ans); //遍历过的假设重心中,最小的最大联通子图的 节点数
    return sum;
}

int main() {
    memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
    cin >> n; //表示树的结点数

    // 题目接下来会输入,n-1行数据,
    // 树中是不存在环的,对于有n个节点的树,必定是n-1条边
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a); //无向图
    }

    dfs(1); //可以任意选定一个节点开始 u<=n

    cout << ans << endl;

    return 0;
}

树与图的宽度优先遍历

例题:https://www.acwing.com/problem/content/849/

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 1e5+10;
int h[N], e[N], ne[N], idx;
int d[N],q[N];//d表示距离,q是队列
int n,m;

int bfs()
{
    int hh = 0,tt = 0;
    q[0] = 1;
    memset(d, -1, sizeof d);
    d[1] = 0;
    
    while(hh <= tt)
    {
        auto t = q[hh++];
        for(int i = h[t];i != -1;i = ne[i])
        {
            int j = e[i];
            if(d[j] == -1)
            {
                d[j] = d[t] + 1;
                q[++tt] = j;
            }
        }
    }
    
    return d[n];
}

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

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;
}

拓扑排序(宽度优先遍历的应用)

只适用于 AOV网 (有向无环图),有环图一定没有拓扑排序

解题思路

  1. 首先记录各个点的入度
  2. 然后将入度为 0 的点放入队列
  3. 将队列里的点依次出队列,然后找出所有出队列这个点发出的边,删除边,同时边的另一侧的点的入度 -1。
  4. 如果指向点的入度变为0后,进入队列
  5. 如果所有点都进过队列,则可以拓扑排序,输出所有顶点。否则输出-1,代表不可以进行拓扑排序。
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

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

void add(int a, int b)  // 添加一条边a->b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool topsort()
{
    int hh = 0,tt = -1;//队列初始化时以及有数时tt=0,空队列tt=-1
    for (int i = 1; i <= n; i ++ )
    {
        if(!d[i])
        {
            q[++tt] = i;//把所有入度为0的数入队
        }
    }
    
    while(hh <= tt)
    {
        int t = q[hh++];
        for(int i = h[t];i != -1;i = ne[i])
        {
            int j = e[i];
            d[j]--;
            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]++;//存储点的入度
    }
    if(topsort())
    {
        for (int i = 0; i < n; i ++ )
        {
            printf("%d ",q[i]);
        }
    }
    else puts("-1");
    return 0;
}

最短路算法

n个点,m个边

单源指一个七点,多源指多个起点

在正边权中,稠密图用朴素D算法(用邻接矩阵存),稀疏图用堆优化版(用邻接表存)

Snipaste_2022-07-18_20-05-58.png

朴素Dijkstra算法

步骤:

  1. 初始化距离,dist[1] = 0,dist[i] = 正无穷
  2. 用集合s存储当前已确定最短距离的点,for循环n次,每次先找到不在s中的距离最近的点t,把t加到s中,然后用t来更新其他点到起点的距离
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=510;

int g[N][N];    //为稠密阵所以用邻接矩阵存储
int dist[N];    //用于记录每一个点距离第一个点的距离
bool st[N];     //用于记录该点的最短距离是否已经确定

int n,m;

int Dijkstra()
{
    memset(dist, 0x3f,sizeof dist);     //初始化距离  0x3f代表无限大

    dist[1]=0;  //第一个点到自身的距离为0

    for(int i=0;i<n;i++)      //有n个点所以要进行n次 迭代
    {
        int t=-1;       //t存储当前访问的点

        for(int j=1;j<=n;j++)   //这里的j代表的是从1号点开始
            if(!st[j]&&(t==-1||dist[t]>dist[j]))     
                t=j;//找到不在s中的距离最近的点t
				if(t == n) break;
        st[t]=true;   

        for(int j=1;j<=n;j++)           //依次更新每个点所到相邻的点路径值
            dist[j]=min(dist[j],dist[t]+g[t][j]);
    }

    if(dist[n]==0x3f3f3f3f) return -1;  //如果第n个点路径为无穷大即不存在最低路径
    return dist[n];
}
int main()
{
    cin>>n>>m;

    memset(g,0x3f,sizeof g);    //初始化图 因为是求最短路径
                                //所以每个点初始为无限大

    while(m--)
    {
        int x,y,z;
        cin>>x>>y>>z;
        g[x][y]=min(g[x][y],z);     //如果发生重边的情况则保留最短的一条边
    }

    cout<<Dijkstra()<<endl;
    return 0;
}

堆优化版Dijkstra算法

堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离
最短的点O(n^2)可以使用最小堆优化。

  1. 一号点的距离初始化为零,其他点初始化成无穷大。
  2. 将一号点放入堆中。
  3. 不断循环,直到堆空。每一次循环中执行的操作为:
    弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
    用该点更新临界点的距离,若更新成功就加入到堆中。
#include<iostream>
#include<algorithm>
#include<cstring>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N=150010;

int h[N], w[N],e[N], ne[N], idx;//为稀疏图所以用邻接表存储,w[N]存储权重
int dist[N];    //用于记录每一个点距离第一个点的距离
bool st[N];     //用于记录该点的最短距离是否已经确定

int n,m;

void add(int a, int b, int c)  // 添加一条边a->b,边权为c
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int Dijkstra()
{
    memset(dist, 0x3f,sizeof dist);     //初始化距离  0x3f代表无限大

    dist[1]=0;  //第一个点到自身的距离为0

    priority_queue<PII,vector<PII>,greater<PII>> heap;//最小堆固定写法
    heap.push({0,1});//heap存距离和编号的键值对,这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
    
    while(heap.size())
    {
        auto t = heap.top();//找到不在s中的距离最近的点t,因为是最小堆,所以堆顶就是最小的
        heap.pop();
        
        int ver = t.second,distance = t.first;
        if(st[ver]) continue;
        st[ver] = true;
        
        for(int i = h[ver];i != -1;i = ne[i])
        {
            int j = e[i];
            if(dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j],j});
            }
        }
    }

    if(dist[n]==0x3f3f3f3f) return -1;  //如果第n个点路径为无穷大即不存在最低路径
    return dist[n];
}
int main()
{
    cin>>n>>m;

    memset(h, -1, sizeof h);

    while(m--)
    {
        int x,y,z;
        cin>>x>>y>>z;
        add(x, y, z);
    }

    cout<<Dijkstra()<<endl;
    return 0;
}

Bellman-Ford算法

   Bellman - ford 算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。

具体步骤:

for n次
	for 所有边 a,b,w (松弛操作)
		dist[b] = min(dist[b],back[a] + w)

注意:back[] 数组是上一次迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点

注意:如果有负权回路,那么最短路不存在。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510,M = 10010;

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

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

int bellman_ford()
{
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i < k; i ++ )
    {
        memcpy(backup,dist,sizeof dist);
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a,b = edges[j].b,w = edges[j].w;
            dist[b] = min(dist[b],backup[a] + w);
        }
    }
    return dist[n];
}

int main()
{
    cin >> n >> m >> k;
    for (int i = 0; i < m; i ++ )
    {
        int a,b,w;
        cin >> a >> b >> w;
        edges[i] = {a,b,w};
    }
    
    int t = bellman_ford();
    
    if(t>=0x3f3f3f3f/2) puts("impossible");
    else printf("%d\n",t);
    
    return 0;
}

SPFA算法

  • spfa算法文字说明:
    1. 建立一个队列,初始时队列里只有起始点。
    2. 再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。
    3. 再建立一个数组,标记点是否在队列中。
    4. 队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且该点不在队列中,则把该点加入到队尾。
    5. 重复执行直到队列为空。
    6. 在保存最短路径的数组中,就得到了最短路径。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 100010;
int h[N], e[N], w[N], ne[N], idx;//邻接表,存储图
int st[N];//标记顶点是不是在队列中
int dist[N];//保存最短路径的值
int q[N], hh, tt = -1;//队列

void add(int a, int b, int c){//图中添加边和边的端点
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa(){
    q[++tt] = 1;//从1号顶点开始松弛,1号顶点入队
    dist[1] = 0;//1号到1号的距离为 0
    st[1] = 1;//1号顶点在队列中
    while(tt >= hh){//不断进行松弛
        int a = q[hh++];//取对头记作a,进行松弛
        st[a] = 0;//从队列中取出来之后该节点st被标记为false,代表之后该节点如果发生更新可再次入队
        for(int i = h[a]; i != -1; i = ne[i])//遍历所有和a相连的点
        {
            int b = e[i], c = w[i];//获得和a相连的点和边
            if(dist[b] > dist[a] + c){//如果可以距离变得更短,则更新距离

                dist[b] = dist[a] + c;//更新距离

                if(!st[b]){//如果没在队列中
                    q[++tt] = b;//入队
                    st[b] = 1;//打标记
                }
            }
        }
    }
}
int main(){
    memset(h, -1, sizeof h);//初始化邻接表
    memset(dist, 0x3f, sizeof dist);//初始化距离
    int n, m;//保存点的数量和边的数量
    cin >> n >> m;
    for(int i = 0; i < m; i++){//读入每条边和边的端点
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);//加入到邻接表
    }
    spfa();
    if(dist[n] == 0x3f3f3f3f )//如果到n点的距离是无穷,则不能到达 
        cout << "impossible";
    else cout << dist[n];//否则能到达,输出距离
    return 0;
}

Floyd算法

#include <iostream>
using namespace std;

const int N = 210, M = 2e+10, INF = 1e9;

int n, m, k, x, y, z;
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]);
}

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--) {
        cin >> x >> y >> z;
        d[x][y] = min(d[x][y], z);
        //注意保存最小的边
    }
    floyd();
    while(k--) {
        cin >> x >> y;
        if(d[x][y] > INF/2) puts("impossible");
        //由于有负权边存在所以约大过INF/2也很合理
        else cout << d[x][y] << endl;
    }
    return 0;
}

最小生成树

Snipaste_2022-07-21_16-21-46.png

稠密图选朴素版Prim算法,稀疏图用Kruskal算法

Prim算法

原理:每次将离连通部分的最近的点和点对应的边加入的连通部分,连通部分逐渐扩大,最后将整个图连通起来,并且边长之和最小。

AcWing 858. Prim算法求最小生成树:图解+详细代码注释(带上了保存路径) - AcWing

#include <iostream>
#include <cstring>
#include <algorithm>

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 ++ )
    {
        int t = -1;
        for(int j = 1;j <= n;j++)
        {
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
            {
                t = j;
            }
        }
        if(i && dist[t] == INF) return INF;
        
        if(i) res+=dist[t];//if(i)指不是第一个点
        for(int j = 1;j <= n;j++) dist[j] = min(dist[j],g[t][j]);

        st[t] = true;
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(g,0x3f,sizeof g);
    while (m -- )
    {
        int a,b,c;
        scanf("%d%d%d", &a, &b,&c);
        g[a][b] = g[b][a] = min(g[a][b],c);
    }
    int t = prim();
    
    if(t == INF) puts("impossible");
    else printf("%d\n",t);
    
    return 0;
}

Kruskal算法

算法思路:

  1. 将所有边按照权值的大小进行升序排序,然后从小到大一一判断。
  2. 如果这个边与之前选择的所有边不会组成回路,就选择这条边分;反之,舍去。
  3. 直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。
  4. 筛选出来的边和所有的顶点构成此连通网的最小生成树。

判断是否会产生回路的方法为:使用并查集。

  1. 在初始状态下给各个个顶点在不同的集合中。、
  2. 遍历过程的每条边,判断这两个顶点的是否在一个集合中。
  3. 如果边上的这两个顶点在一个集合中,说明两个顶点已经连通,这条边不要。如果不在一个集合中,则要这条边。
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200010;

int n,m;
int p[N];

struct Edge
{
    int a,b,w;
    
    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[N];

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 < m; i ++ )
    {
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i] = {a,b,w};
    }
    
    sort(edges,edges+m);
    for (int i = 1; i <= n; i ++ ) p[i] = i;//初始化并查集
    
    int res = 0,cnt = 0;
    for(int i = 0;i < m;i++)
    {
        int a = edges[i].a,b = edges[i].b,w = edges[i].w;
        
        a = find(a),b = find(b);
        if(a != b)
        {
            p[a] = b;
            res += w;
            cnt ++;
        }
    }
    
    if(cnt < n - 1) puts("impossible");
    else printf("%d\n",res);
    
    return 0;
}

二分图

染色法判断是不是二分图

如果判断一个图是不是二分图?

  • 开始对任意一未染色的顶点染色。
  • 判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色。
  • 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断。
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

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

int n,m;
int h[N], e[M], ne[M], idx;
int color[N];//保存各个点的颜色,0 未染色,1 是红色,2 是黑色

void add(int a, int b)  // 添加一条边a->b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool dfs(int u,int c)
{
    color[u] = c;

    //遍历和 u 相邻的点
    for(int i = h[u]; i!= -1; i = ne[i])
    {
        int j = e[i];                   
        if(!color[j])//相邻的点没有颜色,则递归处理这个相邻点
        {
            if(!dfs(j, 3 - c)) return false;//(3 - 1 = 2, 如果 u 的颜色是2,则和 u 相邻的染成 1)
                                            //(3 - 2 = 1, 如果 u 的颜色是1,则和 u 相邻的染成 2)
        }
        else if(color[j] && color[j] != 3 - c)//如果已经染色,判断颜色是否为 3 - c
        {                                     
            return false;//如果不是,说明冲突,返回false             
        }
    }
    return true;
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a,b;
        scanf("%d%d", &a, &b);
        add(a, b),add(b,a);
    }
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
    {
        if(!color[i])
        {
            if(!dfs(i,1))
            {
                flag = false;
                break;
            }
        }
    }

    if(flag) puts("Yes");
    else puts("No");

    return 0;
}

匈牙利算法

匈牙利算法原理

#include <iostream>
#include <cstring>
#include <algorithm>

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]; //match[a] = b: 女生 a 目前匹配了男生 b
bool st[N];// st[a] = true 说明女生 a 目前被一个男生预定了

void add(int a, int b)  // 添加一条边a->b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool find(int x) { //为单身狗 x 找一个对象, (或) x的女朋友被别人预定,给x换一个对象
    // 如果成功,返回true
    for(int i = h[x];i != -1;i = ne[i])
    {
        int j = e[i];
        if (st[j]) continue; // 女生 j 目前被一个男生预定了,跳过它
        st[j] = true; // 将女生 j 预定给男生 x
        // 如果女生 j 没有对象, 或者
        // 女生 j 在前几轮深搜中已预定有对象,但我们成功给她的对象换了个新对象
        if (match[j] == 0 || find(match[j])) {
            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 ++ )// 尝试为每个男生做一轮深搜找对象(要求成功后总匹配数增加1)
    {
        memset(st,false,sizeof st);
        if(find(i)) res++;
    }
    printf("%d\n",res);
    return 0;
}

背包问题

714601465727c6582b5beff904bf539.jpg

0-1背包

版本1 二维
(1)状态f[i][j]定义:前 ii 个物品,背包容量 jj 下的最优解(最大价值):

当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0] = 0开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 ii 件物品的决策,状态f[i][j]不断由之前的状态更新而来。
(2)当前背包容量不够(j < v[i]),没得选,因此前 ii 个物品最优解即为前 i−1i−1 个物品最优解:

对应代码:f[i][j] = f[i - 1][j]。
(3)当前背包容量够,可以选,因此需要决策选与不选第 ii 个物品:

选:f[i][j] = f[i - 1][j - v[i]] + w[i]。
不选:f[i][j] = f[i - 1][j] 。
我们的决策是如何取到最大价值,因此以上两种情况取 max() 。
代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

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

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

一维:

Snipaste_2022-07-27_20-35-13.png

//有优化版
/*
1. f[i] 仅用到了f[i-1]层, 
2. j与j-v[i] 均小于j
3.若用到上一层的状态时,从大到小枚举, 反之从小到大哦
*/
#include <iostream>

using namespace std;

const int N = 1010;

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

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    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] << endl;
 return 0;    
}

完全背包问题

e0da3babb31d38fa7712dd35ab54265.jpg

朴素做法:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

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

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

优化思路:

AcWing 3. 完全背包问题 - AcWing

#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++)
    {
        cin>>v[i]>>w[i];
    }

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

多重背包问题

暴力解法:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110;

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

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

二进制优化:

将S按照二进制分成logs份,每份只能选一次或0次,转化成01背包问题

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 25000,M = 2010;

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

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;
        while(k <= s)
        {
            cnt ++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if(s > 0)
        {
            cnt ++;
            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] << endl;
    return 0;
}

分组背包问题

#include <iostream>
#include <cstring>
#include <algorithm>

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(v[i][k] <= j)
                    f[j] = max(f[j],f[j - v[i][k]] + w[i][k]);
                    
    cout << f[m] << endl;
    return 0;
}

线性DP

例题1:数字三角形

#include<bits/stdc++.h>
using namespace std;

const int N=510,INF=0x3f3f3f3f;
int f[N][N];
int a[N][N];

int main(){
    int n;
    cin>>n;

    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            cin>>a[i][j];
        }
    }

    for(int i=1;i<=n;i++){             
        for(int j=0;j<=i+1;j++){          //因为有负数,所以应该将两边也设为-INF
            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]=a[i][j]+max(f[i-1][j-1],f[i-1][j]);
        }
    }

    int res=-INF;
    for(int i=1;i<=n;i++) res=max(res,f[n][i]);
    cout<<res<<endl;
}

例题2:最长上升子序列

Snipaste_2022-07-28_21-04-23.png

#include <iostream>

using namespace std;

const int N = 1010;

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

int main() {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> w[i];

    int mx = 1;    // 找出所计算的f[i]之中的最大值,边算边找
    for (int i = 0; i < n; i++) {
        f[i] = 1;    // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
        for (int j = 0; j < i; j++) {
            if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1);    // 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
        }
        mx = max(mx, f[i]);
    }

    cout << mx << endl;
    return 0;
}

例题3:最长公共子序列

题解:https://www.acwing.com/solution/content/8111/

#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
  cin >> n >> m >> a + 1 >> b + 1;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i] == b[j]) {
        f[i][j] = f[i - 1][j - 1] + 1;
      } else {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);
      }
    }
  }
  cout << f[n][m] << '\n';
  return 0;
}

区间DP

例题:石子合并

Snipaste_2022-08-01_20-22-52.png

区间 DP 常用模版:

所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)

模板代码如下:

for (int len = 1; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

本题解:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 307;

int a[N], s[N];
int f[N][N];

int main() {
    int n;
    cin >> n;

    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        s[i] += s[i - 1] + a[i];
    }

    memset(f, 0x3f, sizeof f);
    // 区间 DP 枚举套路:长度+左端点 
    for (int len = 1; len <= n; len ++) { // len表示[i, j]的元素个数
        for (int i = 1; i + len - 1 <= n; i ++) {
            int j = i + len - 1; // 自动得到右端点
            if (len == 1) {
                f[i][j] = 0;  // 边界初始化
                continue;
            }

            for (int k = i; k <= j - 1; k ++) { // 必须满足k + 1 <= j
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
            }
        }
    }

    cout << f[1][n] << endl;

    return 0;
}

树形DP

题解

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 6010;
int n;
int happy[N]; //每个职工的高兴度
int f[N][2]; //上面有解释哦~
int e[N],ne[N],h[N],idx; //链表,用来模拟建一个树
bool has_father[N]; //判断当前节点是否有父节点
void add(int a,int b){ //把a插入树中
    e[idx] = b,ne[idx] = h[a],h[a] = idx ++;
}
void dfs(int u){ //开始求解题目
    f[u][1] = happy[u]; //如果选当前节点u,就可以把f[u,1]先怼上他的高兴度
    for(int i = h[u];~i;i = ne[i]){ //遍历树
        int j = e[i];
        dfs(j); //回溯
        //状态转移部分,上面有详细讲解~
        f[u][0] += max(f[j][1],f[j][0]);
        f[u][1] += f[j][0];
    }
}
int main(){
    scanf("%d",&n);
    for(int i = 1;i <= n;i ++) scanf("%d",&happy[i]); //输入每个人的高兴度
    memset(h,-1,sizeof h); //把h都赋值为-1
    for(int i = 1;i < n;i ++){
        int a,b; //对应题目中的L,K,表示b是a的上司
        scanf("%d%d",&a,&b); //输入~
        has_father[a] = true; //说明a他有爸爸(划掉)上司
        add(b,a); //把a加入到b的后面
    }
    int root = 1; //用来找根节点
    while(has_father[root]) root ++; //找根节点
    dfs(root); //从根节点开始搜索
    printf("%d\n",max(f[root][0],f[root][1])); //输出不选根节点与选根节点的最大值
    return 0;
}

贪心

区间选点

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int n;
struct Range
{
    int l,r;
    bool operator< (const Range &W)const
    {
        return r < W.r;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int l,r;
        cin >> l >> r;
        range[i] = {l,r};
    }
    sort(range,range + n);
    
    int res = 0,ed = -2e9;
    for (int i = 0; i < n; i ++ )
    {
        if(range[i].l > ed)
        {
            res++;
            ed = range[i].r;
        }
    }
    cout << res << endl;
    
    return 0;
}
posted @ 2022-08-23 10:30  王ちゃん  阅读(111)  评论(0)    收藏  举报