算法基础课【合集1】

文章目录

  • 基础算法
    • 785. 快速排序
    • 786. 第k个数
    • 787. 归并排序
    • 788. 逆序对的数量
    • 789. 数的范围
    • 790. 数的三次方根
    • 791. 高精度加法
    • 792. 高精度减法
    • 793. 高精度乘法
    • 794. 高精度除法
    • 795. 前缀和
    • 796. 子矩阵的和
    • 797. 差分
    • 798. 差分矩阵
    • 799. 最长连续不重复子序列
    • 800. 数组元素的目标和
    • 2816. 判断子序列
    • 801. 二进制中1的个数
    • 802. 区间和
    • 803. 区间合并
  • 数据结构
    • AcWing 826. 单链表
    • AcWing 827. 双链表
    • AcWing 828. 模拟栈
    • AcWing 3302. 表达式求值
    • AcWing 829. 模拟队列
    • AcWing 830. 单调栈
    • AcWing 154. 滑动窗口
    • AcWing 831. KMP字符串
    • AcWing 835. Trie字符串统计
    • AcWing 143. 最大异或对
    • AcWing 836. 合并集合
    • AcWing 837. 连通块中点的数量
    • AcWing 240. 食物链
    • AcWing 838. 堆排序
    • AcWing 839. 模拟堆
    • AcWing 840. 模拟散列表
    • AcWing 841. 字符串哈希
  • 搜索与图论
    • AcWing 842. 排列数字
    • AcWing 843. n-皇后问题
    • AcWing 844. 走迷宫
    • AcWing 845. 八数码
    • AcWing 846. 树的重心
    • AcWing 847. 图中点的层次
    • AcWing 848. 有向图的拓扑序列
    • AcWing 849. Dijkstra求最短路 I
    • AcWing 850. Dijkstra求最短路 II
    • AcWing 853. 有边数限制的最短路
    • AcWing 851. spfa求最短路
    • AcWing 852. spfa判断负环
    • AcWing 854. Floyd求最短路
    • AcWing 858. Prim算法求最小生成树
    • AcWing 859. Kruskal算法求最小生成树
    • AcWing 860. 染色法判定二分图
    • AcWing 861. 二分图的最大匹配
  • 数学知识-动态规划-贪心【合集2】
  • 时空复杂度分析

基础算法

785. 快速排序

给定你一个长度为 n 的整数数列。

请你使用快速排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1 ∼ 1 0 9 1∼10^9 1109 范围内),表示整个数列。

输出格式
输出共一行,包含 n 个整数,表示排好序的数列。

数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5

#include

using namespace std;

const int N  = 1e5 + 10;

int n;
int q[N];

void quick_sort(int l, int r)
{
    if(l >= r) return;
    
    int x = q[l + r >> 1], i = l  - 1, j = r + 1;
    while(i < j)
    {
        do i++; while(x > q[i]);
        do j--; while(x < q[j]);
        if(i < j) swap(q[i], q[j]);
    }
    
    quick_sort(l, j), quick_sort(j + 1, r);
}

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

786. 第k个数

给定一个长度为 n 的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k 个数。

输入格式
第一行包含两个整数 n 和 k。

第二行包含 n 个整数(所有整数均在 1∼ 1 0 9 10^9 109 范围内),表示整数数列。

输出格式
输出一个整数,表示数列的第 k 小数。

数据范围
1≤n≤100000,
1≤k≤n
输入样例:
5 3
2 4 1 5 3
输出样例:
3

快排过程理解
好题解
二段分界:前一段 < = <= <= 后一段
前一段元素个数 j − l + 1 j - l + 1 jl+1,若 j − l + 1 > = k j - l + 1 >= k jl+1>=k ,则第k大的数在前一段,
否则在后一段中的第 k − ( j − l + 1 ) k - (j - l + 1) k(jl+1)的位置

#include 

using namespace std;

const int N = 100010;

int q[N];

int quick_sort(int q[], int l, int r, int k)//在数组q的区间[l,r]内寻找第k大的数
{
    if (l >= r) return q[l];//递归找到第k大的数【返回第k大的数的下标对应的值】

    int i = l - 1, j = r + 1, x = q[l + r >> 1]; 
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
        //return 可省略
    if (j - l + 1 >= k) return quick_sort(q, l, j, k);//前面整理好的区间元素个数j - l + 1:若比k大则第k大的数在区间[l, j]中
    else return quick_sort(q, j + 1, r, k - (j - l + 1));//反之等效在区间[j + 1, r]中的第k - 前面区间个数(都小于第k大的数)
} 

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

    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    cout << quick_sort(q, 0, n - 1, k) << endl;

    return 0;
}

[error]
nth_element(a, a + k, a + n) 区间第k小的数

nth_element: 把数组元素中第k小的元素放到数组第k位
模板解释:nth_element(数组名,数组名+ k,数组名+元素个数n)

#include 

using namespace std;

typedef long long LL;

const int N = 5e6 + 10;

LL n, k, a[N]; //a可取到1e9
int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    nth_element(a, a + k, a + n);//把第k小的整数排在数组中从小到大的对应位置
    printf("%d",a[k]);//第k的数
    
    return 0;
}

简单暴力:sort

#include 
#include 

using namespace std;

const int N  = 100010;

int n, k;
int q[N];

int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) scanf("%d" , &q[i]);
    sort(q, q + n);//从0开始
    printf("%d", q[k - 1]);
      
    return 0;
}

787. 归并排序

给定你一个长度为 n 的整数数列。

请你使用归并排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1 ∼ 1 0 9 1∼10^9 1109 范围内),表示整个数列。

输出格式
输出共一行,包含 n 个整数,表示排好序的数列。

数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5

二路归并

#include 
#include 
#include 

using namespace std;

const int N = 100010;

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

void merge_sort(int l , int r)
{
    if(l >= r)return;
    int mid = l + r >> 1;
    merge_sort(l , mid) , merge_sort(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;j < k; i ++ , j ++) q[i] = tmp[j];//q数组排序区间[l,r], tmp存放区间[0, k]
    
}

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

788. 逆序对的数量

给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 ia[j],则其为一个逆序对;否则不是。

输入格式
第一行包含整数 n,表示数列的长度。

第二行包含 n 个整数,表示整个数列。

输出格式
输出一个整数,表示逆序对的个数。

数据范围
1≤n≤100000,
数列中的元素的取值范围 [1, 1 0 9 10^9 109]。

输入样例:
6
2 3 4 5 6 1
输出样例:
5

二路归并-逆序对

2022理解版:
i比j小交换,由于单调递增i后面的都比交换后的q[j]=q[i]值大, 但[i, mid]区间 <= [j, r]区间,构成q[j]逆序
即此时j位置的逆序对个数为s[j] = mid - l + 1; 累加s总和即所有逆序对数量

分两段L,R:i,j指针 归并过程判断R段中第j位元素小于L段中元素的数量(逆序)
则i后面均大于j ,即i所在的段 [l,mid] 均大于j , 逆序对数量:(LL)res = mid - i + 1

#include

using namespace std;

typedef long long LL;

const int N = 1e6 + 10;

int tmp[N], q[N]; 
LL res = 0; //注意逆序对数量大!!!
int n;
void merge_sort(int l, int r)
{
    if(l >= r) return;
    int mid = l + r >> 1;
    merge_sort(l, mid), merge_sort(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++];
            res += mid - i + 1;//计算逆序对数量LL res
        }
        
    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(0, n - 1);
    
    cout << res << endl;
    
    return 0;
}

节省空间版


#include 

using namespace std;

const int N = 1e6 + 10;
int tmp[N];
long long res = 0;//注意逆序对数量大!!!

void merge_sort(int q[], int l, int r)  // 归并排序
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else
        {
            res += mid - i + 1;//计算逆序对数量long long
            tmp[k ++ ] = q[j ++ ];
        }
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}


int main()
{
    int n;
    scanf("%d", &n);
    int q[n]; //节省空间版
    for (int i = 0; i < n; i ++) scanf("%d", &q[i]);
    merge_sort(q, 0, n - 1);
    printf("%d\n", res);
    
    return 0;

}

789. 数的范围

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1。

输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1。

数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1

前提有序 − 二分 \color{red}\huge{前提有序-二分} 前提有序二分

答案落的区间 --> 选取两个模板 : if(check(性质)) 选取模板
想清楚check(mid) : 带入条件尝试法
如左到右找第一个x: q[mid] >= x 【找>=x的第一个位置
从右到左找第一个x: q[mid] <= x 【找<=x的第一个位置
bool binary_search(q, q + n, x); 二分查找有序序列中是否存在x
lower_bound(q,q + n, x) - q; 从左到右查找第一个 >= x 的位置注意返回的是地址
upper_bound(q,q + n,x) - q - 1;从左到右查找第一个 > x 的位置(等效从右到左找<=x的第一个位置)

双向二分

//根据题目二分前提:升序排列区间
#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n, m;
int q[N];

int main()
{
    scanf("%d%d", &n, &m);//用m  用q变量名冲突!!! 
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);  //没有&时 :Segmentation Fault   

    while(m --)
    {
        int x;
        scanf("%d", &x);
        // 二分x的左端点
        int l = 0, r = n - 1;   // 确定区间范围
        while (l < r)
        {
            int mid = l + r >> 1;
            if (q[mid] >= x) r = mid; //左->右【单调性从小到大】,x在找到的区间的最大值[l = 0 , x] ,找大于等于x的第一个位置
            else l = mid + 1;
        }

        if (q[r] == x) //若找到左->右的x下标
        {
            cout << r << " "; 

            // 二分x的右端点
            r = n - 1;  // 右端点一定在[左端点, n - 1] 之间   
            while (l < r)
            {
                int mid = l + r + 1 >> 1;   // 因为写的是l = mid,所以需要补上1
                if (q[mid] <= x) l = mid;//右->左【单调性从大到小】 x在找到的区间的最小值[x , r = n - 1] ,找小于等于x的第一个位置
                else r = mid - 1;
            }
            cout << r << endl;
        }
        else puts("-1 -1");
    }

    return 0;
}

S T L 版 \large{STL版} STL
binary_search二分查找函数[前提sort有序]
lower_bound(答案存在区间左边界) + upper_bound函数(答案存在区间右边界)左闭右开

//【左闭右开】区间下标[0, n - 1]
if(binary_search(q, q + n, x)) return true; //bool类型函数[x在区间出现返回真]
else return false;

lower_bound(q, q + n, x):返回查找区间 >= x 的第一个位置
upper_bound(q, q + n, x):返回查找区间 > x 的第一个位置
#include 
#include 

using namespace std;

const int N = 1e5 + 10;
int q[N];

int main()
{
    int n, m;
    scanf("%d%d", &n ,&m);
    for (int i = 0; i < n; i ++) scanf("%d", &q[i]);
        
    while (m --) 
    {
        int x;
        scanf("%d", &x);
        if(binary_search(q, q + n, x))  
        {   //【返回地址 - 首地址】   
            printf("%d %d\n", lower_bound(q, q + n, x) - q, upper_bound(q, q + n, x) - q - 1); 
        }  //左半段右边界  与  右半段左边界  【upper返回第一个比x大的位置,即x在返回位置的前一个位置】
        else 
        {
            cout << "-1 -1" << endl;
        }
    }
    return 0;
}

补充

佬的blog

lower_bound( begin, end, num):从数组的begin位置到end-1位置二分查找第一个 > = n u m >=num >=num的数字,
找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin, end, num):从数组的begin位置到end-1位置二分查找第一个 > n u m >num >num的数字,找到返回该数字的地址,不存在则返回end。
通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

在从大到小的排序数组中重载lower_bound()和upper_bound()
lower_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。
通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。
通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

【评论区hh】:教大家一个口诀 男左女右(判断为true时) 男是一 所以加一 女是零所以不用加

佬:

模板一 m i d = l + r > > 1 ; i f ( 满足性质 )   r = m i d   e l s e   l = m i d + 1 mid = l + r >> 1; if(满足性质)~ r = mid~ else~ l = mid +1 mid=l+r>>1;if(满足性质) r=mid else l=mid+1
我们最终要找的边界是一个性质在右半区符合,在左半区不符合的性质。我们要找符合的右半区的左边界点。
模板二 m i d = l + r + 1 > > 1 ; i f ( 满足性质 )   l = m i d   e l s e   r = m i d − 1 mid = l + r + 1 >> 1; if(满足性质)~ l = mid~ else~ r = mid - 1 mid=l+r+1>>1;if(满足性质) l=mid else r=mid1
我们最终找的边界是一个性质在右半区不符合,在左半区符合的性质。并找到符合性质的左半区的右边界点。

寻找右分界点(左半区的右边界)

整数二分算法模板 —— 模板题 AcWing 789. 数的范围
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}

寻找左分界点(右半区的左边界)

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

封禁用户

790. 数的三次方根

给定一个浮点数 n,求它的三次方根。

输入格式
共一行,包含一个浮点数 n。

输出格式
共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000

浮点数注意用到的变量的类型double

浮点数二分更新均为:l或r = mid;
浮点数精度误差: 循环条件r - l > 1e-8 ——两个浮点数相差极小值时视为相等

x 3 x^3 x3 有单调性可以二分,但不是只有单调性才可以二分[单调性为二分的充分不必要条件]
确定满足二分–>确定区间–>确定二段性(判断条件)

#include

using namespace std;

int main()
{
    double x;
    cin >> x;
    double l = -10000, r = 10000;//区间范围
    while (r - l > 1e-8)//误差精度范围 结果所在区间[l,r] < 1e-8    
    {
        double mid = (l + r) / 2;//double类型不能位运算 >> 1
        if (mid * mid * mid >= x) r = mid; //【找到第一个小于x的数】满足if则说明比x小的在[l, mid],令r = mid 
        else l = mid; 
    }//【浮点数的二分好处,l,r更新都是mid】

    printf("%lf\n", l);//double默认6位

    return 0;
}

既然浮点数这么简单:两个模板都可以用hh

while (r - l > 1e-8)//误差精度范围 结果所在区间[l,r] < 1e-8    
{
    double mid = (l + r) / 2;//double类型不能位运算 >> 1
    if (mid * mid * mid <= x) l = mid; //浮点数的二分好处,都是mid 【找到第一个大于x的数】
    else r = mid;
}

找sqrt(x)——平方根

#include

using namespace std;

int a,b;
int main()  //精度 x开方
{
    
    double x;
    cin >> x;
    
    double l = 0,r = x;
    
    while(r - l > 1e-8)  //达到(区间误差小于)精度1e-8停止
    //for(int i = 0;i < 100;i++)   //二分100次  ,精度很高  = 1 / 2^100  (分成2^100份)
    {
        double mid = (l + r) / 2;
        if(mid * mid >= x) r = mid;  //浮点数简单  都是 mid
        else l = mid;
        
    }
    
    printf("%lf\n",l);
        
    return 0;
}

791. 高精度加法

给定两个正整数(不含前导 0),计算它们的和。

输入格式
共两行,每行包含一个整数。

输出格式
共一行,包含所求的和。

数据范围
1≤整数长度≤100000
输入样例:
12
23
输出样例:
35

模板

#include 
#include 

using namespace std;
//加上引用提高效率:调用函数时不会拷贝一遍A, B,直接引用原地址
vector<int> add(vector<int> &A, vector<int> &B)//小学加法拆解:从个位开始:数相加有进位则下一次需加上进位的1
{
    if (A.size() < B.size()) return add(B, A);//位数多的放前面

    vector<int> C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )//枚举位数多的
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(t);//最后若还有向更高位【指比A或B的最高位更高的1位】进位则再放入一位1  (此时t = 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');//转(int) 且 逆序存放【add函数中即可正序从个位遍历】 
    for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');

    auto C = add(A, B);//auto C忘记写:add中的A.size()会报错

    for (int i = C.size() - 1; i >= 0; i -- ) printf("%d",C[i]);//add返回结果数组仍为逆序存放, 答案数值为C的逆序输出
    puts("");

    return 0;
}

//不用提前判断A, B大小版:每次枚举时判断即可【显然更慢】
//加上引用提高效率:调用函数时不会拷贝一遍A, B,直接引用原地址
vector<int> add(vector<int> &A, vector<int> &B)//小学加法拆解:从个位开始:数相加有进位则下一次需加上进位的1
{

    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);//最后若还有向更高位【指比A或B的最高位更高的1位】进位则再放入一位1
    return C;
}



压9位的代码
看不懂bushi



#include 
#include 

using namespace std;

const int base = 1000000000;

vector<int> add(vector<int> &A, vector<int> &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector<int> C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % base);
        t /= base;
    }

    if (t) C.push_back(t);
    return C;
}

int main()
{
    string a, b;
    vector<int> A, B;
    cin >> a >> b;

    for (int i = a.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (a[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            A.push_back(s);
            s = j = 0;
            t = 1;
        }
    }
    for (int i = b.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (b[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            B.push_back(s);
            s = j = 0;
            t = 1;
        }
    }

    auto C = add(A, B);

    cout << C.back();
    for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);
    cout << endl;

    return 0;
}

792. 高精度减法

给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。

输入格式
共两行,每行包含一个整数。

输出格式
共一行,包含所求的差。

数据范围
1≤整数长度≤ 1 0 5 10^5 105
输入样例:
32
11
输出样例:
21

高精度减法模板

#include
#include

using namespace std;

bool cmp(vector<int> &A, vector<int> &B)//【比较】:判断A是否大于B 
{
    if(A.size() != B.size()) return A.size() > B.size();//位数大的更大【两个正数相减】
    
    for(int i = A.size(); i >= 0; i--)//从最高位开始比较
        if(A[i] != B[i]) 
            return A[i] > B[i];//返回比较结果
    
    return true;
}

vector<int> sub(vector<int> &A, vector<int> &B)  // C = A - B, 满足A >= B, A >= 0, B >= 0
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++ )//从个位开始减
    {
        t = A[i] - t;//t每位值 = A[i] - B[i] - t  减法运算过程对应位相减, 再减去可能有的借位t = 1
        if (i < B.size()) t -= B[i];//对应此位不为0[不超过B长度], 需减去
        C.push_back((t + 10) % 10);//t < 0 为 t + 10 ; t >= 0 为 t  ==>综上: (t + 10) % 10
        if (t < 0) t = 1;//(低位不够减, 向高位借位)  
        else t = 0;//没有借位
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();//去除前导0 : 末尾为0且结果不是0【不止一位:C.size() > 1】
    return C;
}

int main()
{
    string a, b;
    cin >> a >> b;
    vector<int> 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');
    
    vector<int> C;
    if(!cmp(A, B))//输出负数先加负号, 且sub函数只能输出 大 - 小 的结果 :【需交换参数位置】
    {
        printf("-");
        C = sub(B, A);
    }
    else C = sub(A, B);
    
    for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
    puts("");
    
    return 0;
}

793. 高精度乘法

给定两个非负整数(不含前导 0) A 和 B,请你计算 A×B 的值。

输入格式
共两行,第一行包含整数 A,第二行包含整数 B。

输出格式
共一行,包含 A×B 的值。

数据范围
1≤A的长度≤100000,
0≤B≤10000
输入样例:
2
3
输出样例:
6

单高精度乘法模板

#include 
#include 

using namespace std;

vector<int> mul(vector<int> &A, int b)//单高精度乘法
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )//当i = A.size() 但是t != 0 :即最高位有进位 需继续存入
    {
        if (i < A.size()) t += A[i] * b;//如果是更高位的进位, 则不执行此步 【否则越界】
        C.push_back(t % 10);
        t /= 10;
    }
    //去除前导0:【若为0判断:如果是结果为0,则C.size() = 1,反之C.size() >1】
    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}


int main()
{
    string a;//单高精度乘法模板
    int b;

    cin >> a >> b;

    vector<int> A;
    for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');//string --> int 放入vector : push_back() 

    auto C = mul(A, b); 

    for (int i = C.size() - 1; i >= 0; i -- ) printf("%d", C[i]);

    return 0;
}

794. 高精度除法

给定两个非负整数(不含前导 0) A,B,请你计算 A/B 的商和余数。

输入格式
共两行,第一行包含整数 A,第二行包含整数 B。

输出格式
共两行,第一行输出所求的商,第二行输出所求余数。

数据范围
1≤A的长度≤100000,
1≤B≤10000,
B 一定不为 0
输入样例:
7
2
输出样例:
3
1

单高精度除法

#include 
#include 
#include //reverse

using namespace std;

vector<int> div(vector<int> &A, int b, int &r)//需要返回计算后的余数r :加引用 &r
{
    vector<int> C;
    r = 0;//余数
    for (int i = A.size() - 1; i >= 0; i -- )//小学除法计算最高位开始除
    {
        r = r * 10 + A[i];//把上一位的余数 * 10 + 当前位 , 再做除法
        C.push_back(r / b);//放入每位除法结果 
        r %= b;//计算当前余数
    }
    reverse(C.begin(), C.end());//为了统一逆序存放main中逆序输出:用reverse翻转vector [C.begin(),C.end()]
    while (C.size() > 1 && C.back() == 0) C.pop_back();//C.isze() > 1 说明结果不为0,若此时C.back() == 0则为前导0
    return C;
}

int main()
{
    string a;
    vector<int> A;

    int B;
    cin >> a >> B;
    for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');

    int r;
    auto C = div(A, B, r);

    for (int i = C.size() - 1; i >= 0; i -- ) printf("%d", C[i]);

    printf("\n%d", r);//cout << endl << r << endl;
    
    return 0;
}

795. 前缀和

输入一个长度为 n 的整数序列。

接下来再输入 m 个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。

输出格式
共 m 行,每行输出一个询问的结果。

数据范围
1≤l≤r≤n,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10

前缀和模板

#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N];   // 表示原数组
int s[N];   // 表示前缀和数组

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

    for (int i = 1; i <= n; i ++ )//用到下标i-1 ,下标从1开始
    {
        scanf("%d", &a[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;
}

节省空间,不保存原数组:边读入边初始化前缀和数组s[i]

#include 

using namespace std;

const int N = 100010;

int n, m;
int s[N];   // 表示前缀和数组

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

    for (int i = 1; i <= n; i ++ )//用到下标i-1 ,下标从1开始
    {
        scanf("%d", &s[i]);
        s[i] += s[i - 1];//前缀和数组
    }

    while (m -- )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]);
    }

    return 0;
}

796. 子矩阵的和

输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。

对于每个询问输出子矩阵中所有数的和。

输入格式
第一行包含三个整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式
共 q 行,每行输出一个询问的结果。

数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21

二维前缀和:容斥原理

构造二维前缀和矩阵 : S x , y = S x − 1 , y + S x , y − 1 − S x − 1 , y − 1 + a x , y S_{x,y} = S_{x-1,y} + S_{x,y-1} - S_{x-1,y-1} + a_{x,y} Sx,y=Sx1,y+Sx,y1Sx1,y1+ax,y
用前缀和矩阵得到子矩阵的和 :给出(x1,y1)(x2,y2)求和: S x 2 , y 2 − S x 1 − 1 , y 2 − S x 2 , y 1 − 1 + S x − 1 , y − 1 S_{x2,y2} - S_{x1-1,y2} - S_{x2,y1-1} + S_{x-1,y-1} Sx2,y2Sx11,y2Sx2,y11+Sx1,y1

#include 
#include 
#include 
#include 

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]);
            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);//边界:[较小的 - 1] : (直线x1 - 1) 和 (直线y1 - 1)
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);//子矩阵求和
    }

    return 0;
}

不保存原数组版:边读入边初始前缀和

#include 

using namespace std;

const int N = 1010;

int n, m, q;
int 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", &s[i][j]);
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
        }

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

797. 差分

输入一个长度为 n 的整数序列。

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

请你输出进行完所有操作后的序列。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。

输出格式
共一行,包含 n 个整数,表示最终序列。

数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2

差分-前缀和逆运算
差分与前缀和为互逆运算 ==> b[]为a[]差分数组 , a[]为b[]的前缀和数组

原数组a,差分数组b
使得 a[i] = b[1] + b[2 ] + … + b[i] ==> b[i] = a[i] - a[i-1];
不直接对原数组a运算,而是用差分数组b运算 ,最后再赋值给a ==> a[i] = a[i - 1] + b[i]

#include

using namespace std;

const int N= 100010;

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

int main()
{
    cin >> n >> m; 
    for(int i = 1; i <= n; i++) //把数组a看做差分数组b的前缀和数组, 则还原b[i] = a[i] - a[i - 1];
    {
        scanf("%d", &a[i]); 
        b[i] = a[i] - a[i - 1];//用到前缀和构造差分b[i],有i-1下标,下标从1开始
    }
    int l, r, c;
    while (m--)//m次操作, 在a数组区间[l, r]上的数加上c:操作差分数组b O(1)达到此效果 : 重新读取操作后的a数组 
    {
        scanf("%d%d%d", &l, &r, &c);
        b[l] += c, b[r + 1] -= c;     //将序列中区间[l, r]的每个数都加上c 
    }//区间[l, r] += c : 从l开始均被影响+c,知道r+1时-c相互抵消,r+1项后数值不变【前缀和包括前面项】
     for (int i = 1; i <= n; i++)
    {
        a[i] = b[i] + a[i - 1];    //重新计算操作后的前缀和数组a[i] 
        printf("%d ", a[i]);//求操作完的数组a[i]
    }
    return 0;
}

y总写法 :节省原数组a - 逆序初始赋值差分s[]
核心变化:
for (int i = n; i; i – ) s[i] -= s[i - 1];
for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];

#include 

using namespace std;

const int N = 100010;

int n, m;
int s[N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> s[i];
    for (int i = n; i; i -- ) s[i] -= s[i - 1];//逆序赋值求差分数组b[i] = a[i] - a[i - 1]

    while (m -- )
    {
        int l, r, c;
        cin >> l >> r >> c;
        s[l] += c, s[r + 1] -= c;//区间[l, r] += c : 从l开始均被影响+c,知道r+1时-c相互抵消,r+1项后数值不变【前缀和包括前面项】
    }

    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];//前缀和数组a[i] = b[i] + a[i - 1]

    for (int i = 1; i <= n; i ++ ) cout << s[i] << ' ';
    cout << endl;

    return 0;
}
/*
void insert(int l,int r, int c)
{
    b[l] += c, b[r + 1] -= c;
}
*/

798. 差分矩阵

输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c。

请你将进行完所有操作后的矩阵输出。

输入格式
第一行包含整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。

输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。

数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2

二维差分b[][]
超级省数组:b二维差分数组直接运算前缀和恢复成原数组【差分后再恢复即得到操作后的原数组】
insert(i, j, i, j, c) 等效原数组[i][j] = c,且实际操作b得到差分数组
二维差分好题解-图片来源
算法基础课【合集1】_第1张图片

#include 

using namespace std;

const int N = 1010;

int n, m, q;
int b[N][N];//省数组:直接边读入边差分

void insert(int x1, int y1, int x2, int y2, int c)//二维差分函数 【操作区域:影响区域】
{
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;//更高的不受影响的边界: x2 + 1 与 y2 + 1
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;//(x2 + 1 , y2 + 1)被连续减两次c : 需加回c才不受影响
}

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

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            int x;
            scanf("%d", &x);//原数组直接存入差分数组b, 边读入边差分, 再求前缀和数组:b自身累加覆盖原差分数组b
            insert(i, j, i, j, x);//初始化差分数组b[i][j]:点(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];//还原操作后的a[][] 为差分数组b[][]的前缀和:直接计算b
            
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ ) printf("%d ", b[i][j]);
        puts("");
    }

    return 0;
}

保留原数组版

#include 

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];//原数组a[][] 二维差分数组b[][]

void insert(int x1, int y1, int x2, int y2, int c)//二维差分函数 【操作区域:影响区域】
{
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;//更高的不受影响的边界: x2 + 1 与 y2 + 1
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;//(x2 + 1 , y2 + 1)被连续减两次c : 需加回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]);//读入原数组a[][] 边读入边差分
            insert(i, j, i, j, a[i][j]);//初始化差分数组b[i]:只影响(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 ++ )//等号连用全部嘎嘎改
            a[i][j] = b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];//还原操作后的a[][] 为差分数组b[][]的前缀和:直接计算b
    //a[i][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]);//此时b或a都行
        puts("");
    }

    return 0;
}

不封装-内嵌版

#include 

using namespace std;

typedef long long ll;

const int N = 1010;
const int inf = 0x3f3f3f;

int n, m, q;
int a[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 ++ )//初始化二维差分数组
        {
            int x;
            scanf("%d", &x);
            a[i][j] += x;
            a[i + 1][j] -= x;
            a[i][j + 1] -= x;
            a[i + 1][j + 1] += x;
        }

    while (q -- )
    {
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        a[x1][y1] += c;
        a[x2 + 1][y1] -= c;
        a[x1][y2 + 1] -= c;
        a[x2 + 1][y2 + 1] += c;
    }

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];//把差分数组转换成前缀和数组

    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ ) printf("%d ", a[i][j]);
        puts("");
    }

    return 0;
}

799. 最长连续不重复子序列

给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

输入格式
第一行包含整数 n。

第二行包含 n 个整数(均在 0∼105 范围内),表示整数序列。

输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围
1≤n≤ 1 0 5 10^5 105
输入样例:
5
1 2 2 3 5
输出样例:
3

算法基础课【合集1】_第2张图片

利用单调性-双指针

#include 

using namespace std;

const int N = 100010;

int n;
int q[N], s[N];//s统计对应元素值的个数

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

    int res = 0;
    for (int i = 0, j = 0; i < n; i ++ )
    {
        s[q[i]] ++ ;//对应元素个数++
        while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;//遇到重复元素:j移动看能到多远不重复,j往前移动,元素出队个数-- [左端点j < 右端点i]
        res = max(res, i - j + 1);//更新不重复区间:属性max
    }

    cout << res << endl;

    return 0;
}

扩展:双指针读取单词 abc def ghi

#include
#include//gets

using namespace std;

const int N = 1010;

char s[N];     

int main()
{
    while(~scanf("%s", s)) puts(s);//语法题:一行搞定法 
    // gets(s);//读一行 【gets编译器升级后不能用了】
    // int n = strlen(s);
    // for(int i = 0; i < n; i++)
    // {
    //     while(j < n && s[j] != ' ') j++; //停止时指向空格位置
    //     printf("%s", substr(s[i], j - 1 - i + 1)); //for(int k = i; k < j; k++ ) printf("%c", s[i]);
    //     i = j;    
    // }
    return 0;
}

800. 数组元素的目标和

给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。

数组下标从 0 开始。

请你求出满足 A[i]+B[j]=x 的数对 (i,j)。

数据保证有唯一解。

输入格式
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。

第二行包含 n 个整数,表示数组 A。

第三行包含 m 个整数,表示数组 B。

输出格式
共一行,包含两个整数 i 和 j。

数据范围
数组长度不超过 1 0 5 10^5 105
同一数组内元素各不相同。
1≤数组元素≤ 1 0 9 10^9 109
输入样例:
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:
1 1

从前往后和从后往前 [具有单调性]

mycode:

#include 
#include 
#include 

using namespace std;

const int N = 100010;

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

int main()
{
    int x;
    cin >> n >> m >> x;
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    for(int i = 0; i < m; i++) scanf("%d", &b[i]);
    
    for(int i = 0, j = m - 1; i < n && j >= 0 ; i++) //a从小到大枚举 , b从大到小枚举 
    {
            
        while(a[i] + b[j] > x) j--;//大于x, b往前(j指针往左)移动
        
        if(a[i] + b[j] == x) 
        {
            printf("%d %d", i, j);
            break;//return 0;
        }
    }
    
    return 0;
}

y总的:

#include 

using namespace std;

const int N = 1e5 + 10;

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

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

    for (int i = 0, j = m - 1; i < n; i ++ )
    {
        while (j >= 0 && a[i] + b[j] > x) j -- ;
        if (j >= 0 && a[i] + b[j] == x)
        {
            cout << i << ' ' << j << endl;
            break;
        }
        
    }

    return 0;
}

2816. 判断子序列

给定一个长度为 n 的整数序列 a1,a2,…,an 以及一个长度为 m 的整数序列 b1,b2,…,bm。

请你判断 a 序列是否为 b 序列的子序列。

子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5} 是序列 {a1,a2,a3,a4,a5} 的一个子序列。

输入格式
第一行包含两个整数 n,m。

第二行包含 n 个整数,表示 a1,a2,…,an。

第三行包含 m 个整数,表示 b1,b2,…,bm。

输出格式
如果 a 序列是 b 序列的子序列,输出一行 Yes。

否则,输出 No。

数据范围
1≤n≤m≤ 1 0 5 10^5 105,
− 1 0 9 −10^9 109≤ai,bi≤ 1 0 9 10^9 109
输入样例:
3 5
1 3 5
1 2 3 4 5
输出样例:
Yes

双指针-细节思想

//暴力枚举 check O(nm) > 1e8 
#include

using namespace std;

const int N = 100010;

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

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++) scanf("%d", &p[i]);
    for(int i = 0; i < m; i++) scanf("%d", &s[i]);
    
    int cnt = 0;
    for(int i = 0; i < m; i ++)//i为p子序列指针, j为s序列指针
    {
        if(cnt < n && s[i] == p[cnt]) cnt ++;//匹配成功就让i ++   【cnt < n因为如果有重复数值匹配就会多加】
        //但是可以把判断条件改成 cnt >= n即匹配成功全部都有 : p的元素s都有
    }
    
    if(cnt >= n) puts("Yes");  //cnt >= n说明全部都有, 大于n说明有重复的 : 前面加上cnt < n的限制则cnt最大为n    
    else puts("No");
    
    return 0;
}

此题扩展好文

801. 二进制中1的个数

给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。

输入格式
第一行包含整数 n。

第二行包含 n 个整数,表示整个数列。

输出格式
共一行,包含 n 个整数,其中的第 i 个数表示数列中的第 i 个数的二进制表示中 1 的个数。

数据范围
1≤n≤100000,
0≤数列中元素的值≤ 1 0 9 10^9 109
输入样例:
5
1 2 3 4 5
输出样例:
1 1 2 1 2

x >> i & i [判断x二进制数表示中第i位上数值]【从0开始】

if(x >> i & i) = true :二进制数表示的第i位为1 , 反之为0
#include

using namespace std;

int n;

int main()
{
    scanf("%d", &n);
    int x;
    while (n -- )
    {
        scanf("%d", &x);
        int cnt = 0;
        while(x)
        {
            if(x & 1) cnt ++;//二进制数个位若为1 : x & 1 = true ==> cnt ++
            x >>= 1;
        }
        printf("%d ", cnt);
    }
    
        
    return 0;
}

lowbit()

#include

using namespace std;

int n;

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

int main()
{
    scanf("%d", &n);
    int x;
    while (n -- )
    {
        scanf("%d", &x);
        int cnt = 0;
        while(x)
        {
            x -= lowbit(x);
            cnt ++;
        }
        printf("%d ", cnt);
    }
        
    return 0;
}

802. 区间和

假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。

现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。

接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。

输入格式
第一行包含两个整数 n 和 m。

接下来 n 行,每行包含两个整数 x 和 c。

再接下来 m 行,每行包含两个整数 l 和 r。

输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。

数据范围
1 0 9 10^9 109≤x≤ 1 0 9 10^9 109,
1≤n,m≤ 1 0 5 10^5 105,
1 0 9 10^9 109≤l≤r≤ 1 0 9 10^9 109,
−10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5

离散化-压缩区间
核心思想:先离散化 再前缀和
离散化 : 存储不连续的分散的值 用下标映射 : hash(index, value) : 如(1, 1), (2, 10), (3, 11), (4, 100000)
注意问题:
①数组a中可能存在重复元素 : 【去重】 过程保证a有序 : 可满足严格单调递增
②如何算出下标离散化之后的值 : 插入位置下标 与 查询区间下标都要离散化:对应离散化之后的区间查询
所有坐标离散化, 则查询原坐标的区间[l , r] : l , r 也要离散化对应离散化的区间值才等效

具体操作

初始化
add存入{x, c} , query存入{l, r}
alls存入所有插入下标x , 区间左端点l , 右端点r

离散化
①离散化alls可变长数组中存放的所有要用到的坐标
去重【去的是重复下标】: alls.erase(unique(begin, end)返回去重后最后一个下标的位置, 原end)
erase:删除unique筛出的重复下标
②find函数查找下标x离散化后对应的alls中下标【sort单调存储 :用二分查找O(logn)】
遍历存放 add存放的{x, c} 运用find查找alls中x离散化后的位置, 初始化数组a : a[x] += item.y
[for(auto item : add)]
③得到数组a后, 初始化前缀和数组s , 再遍历 query存放的{l, r} :输出区间和: s[r] - s[l - 1]
[for(auto item : query)]

#include 
#include 
#include 

#define x first//pair简化代码
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;//要离散化插入坐标 + 区间左右端点 

int n, m;
int a[N], s[N]; //离散化后的数组a , 对应前缀和数组s

vector<int> alls;//存所有要用到的下标: 插入位置下标x 、 查询区间的边界下标: l , R
vector<PII> add, query;//(index, value)

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;//找到大于等于x的最小的数:用第一个模板
        else l = mid + 1;
    }
    return r + 1;//加一:映射到从1开始(因为前缀和)
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )//存插入的操作组
    {
        int x, c;
        scanf("%d%d", &x, &c);
        add.push_back({x, c}); //在下标x的位置上加c : 放入原下标x :之后进行离散化处理

        alls.push_back(x);//存下标
    }

    for (int i = 0; i < m; i ++ )//存查询的操作组
    {
        int l, r;
        scanf("%d%d", &l, &r);
        query.push_back({l, r});//询问的区间

        alls.push_back(l);//所有要用到的区间的左右端点下标放入alls
        alls.push_back(r);
    }

    // 去重【去的不是重复的value, 而是重复的index!!!!!!】
    sort(alls.begin(), alls.end());//从小到大
    alls.erase(unique(alls.begin(), alls.end()), alls.end());//把这些下标整理
    //unique会把重复元素放到后面:筛选出不重复段返回结束下标, 再用erase删除unique整理出的重复index段[返回的新数组end(),原数组end()]
    
    //对指定下标x添加元素c 【x转化为离散化映射的下标, c值不变】
    for (auto item : add)
    {
        int x = find(item.x);
        a[x] += item.y;//a存离散化后的x位置加c的操作构造的数组
    }

    // 预处理前缀和
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];

    // 处理询问:取query的查询区间
    for (auto item : query)//求[L, R]的值的和
    {
        int l = find(item.x), r = find(item.y);//查找离散化后对应的区间
        printf("%d\n", s[r] - s[l - 1]);//用前缀和数组s求区间和
    }

    return 0;
}

803. 区间合并

给定 n 个区间 [li,ri],要求合并所有有交集的区间。

注意如果在端点处相交,也算有交集。

输出合并完成后的区间个数。

例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。

输入格式
第一行包含整数 n。

接下来 n 行,每行包含两个整数 l 和 r。

输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围
1≤n≤100000,
1 0 9 10^9 109≤li≤ri≤ 1 0 9 10^9 109
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3

区间合并思想:按左端点sort区间 : 判断每两个相邻的区间是否有交集

按左端点排序后的区间:每两个区间有三种情况:
    ①[完全包含]:更长的区间继续判断下一个
    ②[部分包含]:合并区间, 更新右端点为更大的
    ③[没有交集]:直接把前一个区间加入集合

【左端点排序版】

#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

vector<PII> segs;//存放区间

void merge(vector<PII> &segs)//归并区间
{
    vector<PII> res;

    sort(segs.begin(), segs.end());//默认first排序, 即按左端点从小到大排序
    //按右端点排序则初始边界为2e9
    int st = -2e9, ed = -2e9;//初始判断边界 [刚开始没有区间保证左端右点一定更新成第一个区间:则初始{st, ed}需取边界最小值]
    for (auto seg : segs)
        if (ed < seg.x)//上一个的右端点ed < 当前区间左端点st ==> 没有交集:加入res
        {
            if (st != -2e9) res.push_back({st, ed});//st = -2e9为边界保证更新,不能把边界加入进去((非真实区间)
            st = seg.x, ed = seg.y;//更新下一个判断的区间的左右端点{st, ed} = {l, r} = {segs.x, segs.y}
        }
        else ed = max(ed, seg.y); //有重叠则两个区间合并:每个区间长度不同:选取右端点最大的作为右边界
    //若为空则为0,不加入任何区间(不能加边界哦)
    if (st != -2e9) res.push_back({st, ed});//最后的区间没有后序区间合并, 需直接加入最后一个区间 
    //佬: 有两个作用,1.是防止n为0,把[-无穷,-无穷]压入;2.是压入最后一个(也就是当前)的区间,若n>=1,if可以不要
    segs = res;
}

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

    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});//初始化存入所有区间左右端点
    }

    merge(segs);//归并区间

    cout << segs.size() << endl;

    return 0;
}

封禁用户

#include 
using namespace std;
int n, cnt = 0;
struct S {
    int l, r;
} a[100010], ans[100010];

int cmp(S a, S b) {//重构小于号:比较规则:枚举左端点
    if (a.l == b.l) return a.r < b.r;
    return a.l < b.l;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].l, &a[i].r);//从1开始
    sort(a + 1, a + 1 + n, cmp);
    ans[++cnt] = a[1];//先放入第一个区间
    for (int i = 2; i <= n; i++) 
    {
        if (a[i].l <= ans[cnt].r) ans[cnt].r = max(ans[cnt].r, a[i].r);//【改变ans中已存入的】
        else ans[++cnt] = a[i];
    }
    cout << cnt << endl;
    return 0;
}

数据结构

AcWing 826. 单链表

实现一个单链表,链表初始为空,支持三种操作:

向链表头插入一个数;
删除第 k 个插入的数后面的数;
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式
第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

1.H x,表示向链表头插入一个数 x。
2.D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
3.I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。

数据范围
1≤M≤100000
所有操作保证合法。

输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5

机试 - 链式前向星【数组模拟链表】

数组模拟链表:静态链表
动态链表超时:new申请空间耗时。

head :存头结点下标
e[idx] :下标为idx的节点的值
ne[idx] :idx的下一个节点的编号
idx :【从0开始】存储当前用到了哪个节点[下一个节点使用的下标]
此题询问k是从1开始, 则对应模拟链表的下标为k-1
head当做指针,非空时(非-1)指向存放的第一个结点(头结点)的下标,初始下标-1代表指向null

#include 
#include 

using namespace std;

const int N = 100010;

int head, e[N], ne[N], idx;//第一个元素下标从0开始【当前idx处于未使用状态-直接用于新插入节点, 再idx++】

void init()
{   //【head当做指针,非空时(非-1)指向存放的第一个结点(头结点)下标, 初始下标-1代表指向null】
    head = -1;//head_idx == -1 
}

void add_head(int x) //头插法[插在第一个节点(头结点)位置,head指向新头结点下标地址]
{
    e[idx] = x, ne[idx] = head, head = idx ++ ;
}

void add_k(int k, int x) //将x插到下标是k的点后面 [下标从0开始]
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}

void remove(int k) //删除第k个节点之后的节点
{
    ne[k] = ne[ne[k]];
}

int main()
{
    init();

    int m;
    cin >> m;
    while (m -- )
    {
        char op;
        int k, x; //此题第k节点编号从1开始, 对应模拟链表的下标为k - 1
        cin >> op;
        if (op == 'H')
        {
            cin >> x;
            add_head(x);//向链表头插入一个数 x。
        }
        else if (op == 'I')
        {
            cin >> k >> x;
            add_k(k - 1, x);//在第 k 个插入的数后面插入一个数 x
        }
        else //删除-需特判
        {
            cin >> k;//第k个插入的点,下标k-1
            if (!k) head = ne[head];//特判删除第一个结点(头结点) 【防止下标k-1 < 0越界】
            else remove(k - 1);//删除第k个点
        }
    }
    //最后一个节点的ne存下标-1 代表null
    for (int i = head; i != -1; i = ne[i]) printf("%d ", e[i]); //从head开始按ne[i] (后继元素下标) 遍历链表
    puts("");

    return 0;
}

不错哟

大佬的双链表
考研笔试版【new Node()】

【待补充单链表版】此为双链表版

#include

using namespace std;

struct Node
{
	int val;
	Node *prev , * next;  //双链表 
	
	//构造函数
	Node() : prev(NULL) ,next(NULL) { } 
	Node(int _val) :val(_val) ,prev(NULL) ,next(NULL) { }  //new Node创建新结点:传入参数_val ,赋值给val , 同时next = NULL 
	  
}; 

void print(Node* head)
{
	for(auto p = head ; p ;p = p->next)
		printf("%d ",p->val);
	puts("");
}

int main()
{
	//初始化双链表  Node*类型指针 [哨兵,左右护法,不会用到值,判断边界]
	Node *head  = new Node() , *tail = new Node();   //可以不赋值 ,但NULL奇怪 
	head->next = tail , tail->prev = head ; //空,头尾指针互指 
	
	print(head);
	
	auto a = new Node(1); //加入新结点 
	a->next = head->next , a->prev = head;    //顺序!! 【先把新结点的指针连好,再把头尾指向新结点】 
	head->next = a , tail->prev = a; 
	
	print(head);
	
	return 0;
 } 

双链表

【不要死记硬背 ,用图翻译代码】

#include

using namespace std;

struct Node
{
	int val;
	Node *prev , * next;  //双链表 
	
	//构造函数
	Node() : prev(NULL) ,next(NULL) { } 
	Node(int _val) :val(_val) ,prev(NULL) ,next(NULL) { }  //new Node创建新结点:传入参数_val ,赋值给val , 同时next = NULL 
	  
}; 

void print(Node* head) //循环双链表打印 
{
	for(auto p = head->next; p != head ;p = p->next)  //next哨兵,不存元素 , 从next开始走,走回next停止 【循环双链表】 
		printf("%d ",p->val);
	puts("");
}

int main()
{
	//初始化双链表  Node*类型指针 [哨兵,左右护法,不会用到值,判断边界]
	Node *head  = new Node() , *tail = head;   //构造循环双链表简单【仅需tail与head指针指向同一个点】;new头结点可以不赋值 ,但编译器问题NULL = 13703728 
	head->next = tail , tail->prev = head ; //空,头尾指针互指 
	
	//print(head);
	
	auto a = new Node(1); //加入新结点    [头插法不要用tail,很多节点后在十万八千里]
	a->next = head->next , a->prev = head;    //顺序!! 【先把新结点的指针连好,再把头尾指向新结点】 
	head->next = a , tail->prev = a; 
	
	//print(head);
	
	auto b = new Node(2);
	b->next = a->next , b->prev = a;  //b->a , b->a->next  [插入a后面]   
	a->next = b, head->next = a;  //双向互指  a更改前后驱
	
	print(head); // 1 2
	
	//删除b 【用b指针】 
	b->prev->next = b->next;  //[前驱的后指针指向后后,后驱的前指针->prev指向前前,]
	b->next->prev = b->prev;
	delete b; 
	
	print(head); // 1
	
	auto c = new Node(3); //c插入a的前面 (头插法)  [此时:head->next = a , a->prev = head;]
	c->next = a , c->prev = head; 
	head->next = c, a->prev = c; 
	
	print(head); // 3 1
	
	return 0;
 } 

AcWing 827. 双链表

实现一个双链表,双链表初始为空,支持 5 种操作:

1.在最左侧插入一个数;
2.在最右侧插入一个数;
3.将第 k 个插入的数删除;
4.在第 k 个插入的数左侧插入一个数;
5.在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式
第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

1.L x,表示在链表的最左端插入数 x。
2.R x,表示在链表的最右端插入数 x。
3.D k,表示将第 k 个插入的数删除。
4.IL k x,表示在第 k 个插入的数左侧插入一个数。
5.IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式
共一行,将整个链表从左到右输出。

数据范围
1≤M≤100000
所有操作保证合法。

输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9

idx : 指针下标编号
e[N] : 存结点值
l[N] :l数组存左边指向的点下标【前驱】
r[N] :r数组存右边指向的点下标【后继】
模拟双链表【0为左端点, 1为右端点, idx从2开始】
初始化 r[0] = 1, l[1] = 0; idx = 2; **左端0的右边指向右端点1, 右端点1的左边指向左端点0 **

题意的k:指代第 k 个插入的结点

(相似模板化-有助于记忆)
算法基础课【合集1】_第3张图片

#include 

using namespace std;

const int N = 100010;

int m; 
int e[N], l[N], r[N], idx; //l存左边指向的点下标【前驱】, r存右边指向的点下标【后继】

void init()
{//head <--> tail 根据模拟左右插入必须定【0左, 1右】
    //0是左端点(head),1是右端点(tail) 
    r[0] = 1, l[1] = 0; //左端0的右边指向右端点1, 右端点1的左边指向左端点0 【0左 <-- 1右, 0左 --> 1右】
    idx = 2; //【注意0和1被用掉了, 下标从2开始】
}

// 在节点k的右边插入一个数x
void insert(int k, int x)
{
    e[idx] = x;     //赋值
    l[idx] = k;     //新结点左侧指向k
    r[idx] = r[k];  //新结点右侧指向k的右侧
    l[r[k]] = idx;  //k的右侧节点的左边指向新结点
    r[k] = idx ++ ; //k的右侧指向新结点, 同时编号idx++
}

// 删除节点k
void remove(int k)
{
    l[r[k]] = l[k]; //k左测的右边 = 左侧 
    r[l[k]] = r[k]; //k右侧的左边 = 右侧
}

int main()
{
    init(); //习惯化 :初始化写第一句!!!
    
    cin >> m;
    
    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); //idx从2开始:删除第k个插入对应idx为k + 1
        }
        else if (op == "IL") //在第 k 个插入的数左侧插入一个数
        {
            cin >> k >> x;
            insert(l[k + 1], x); //idx从2开始:第k个插入对应idx为k + 1
        }
        else //在第 k 个插入的数右侧插入一个数
        {
            cin >> k >> x;
            insert(k + 1, x);
        }
    }

    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}

压行简写

// 在节点k的右边插入一个数x 
void insert(int k, int x)
{
   e[idx] = x; 
   l[idx] = k, r[idx] = r[k];
   l[r[k]] = idx, r[k] = idx ++;      // l[r[k]] = idx   内嵌翻译右侧节点 的 左边指向 idx
}

AcWing 828. 模拟栈

实现一个栈,栈初始为空,支持四种操作:

push x – 向栈顶插入一个数 x;
pop – 从栈顶弹出一个数;
empty – 判断栈是否为空;
query – 查询栈顶元素。
现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式
第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。

输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围
1≤M≤100000,
1≤x≤ 1 0 9 10^9 109
所有操作保证合法。

输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO

数组模拟栈-模板

#include 

using namespace std;

const int N = 100010;

int m;
int stk[N], tt;//栈顶指针tt :【初始状态 tt == 0 为空】

int main()
{
    cin >> m;
    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            scanf("%d", &x);
            stk[ ++ tt] = x;
        }
        else if (op == "pop") tt -- ;
        else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;//【条件表达式】表达式返回值 ? 为真执行1 : 为假执行2  
        else printf("%d\n", stk[tt]);
    }

    return 0;
}

无聊写的不是

#include
#include

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

int n;
stack<PII> s; 

int main()
{
    scanf("%d", &n);
    
    for(int i = n; i >= 1; i--)  s.push({i, i});//逆序存入, 正序输出
            
        while(s.size())   
        {
          printf("%d %d\n", s.top().x, s.top().y);
          s.pop();
        }
            
    return 0;
}

AcWing 3302. 表达式求值

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

数据保证给定的表达式合法。
题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
题目保证表达式中所有数字均为正整数。
题目保证表达式在中间计算过程以及结果中,均不超过 2 31 − 1 2^{31}−1 2311
题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式
共一行,为给定表达式。

输出格式
共一行,为表达式的结果。

数据范围
表达式的长度不超过 1 0 5 10^5 105

输入样例:
(2+2)*(1+1)
输出样例:
8

表达式求值【数学正常书写顺序】

isdigit(s[i]) : 双指针处理秦九韶提取数字入num栈
eval()计算表达式函数:第一个操作数a后出栈 op操作符c 第二个操作数b先出栈
优先级表初始化 unordered_map pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
优先级高的先计算完: 若当前运算符优先级 <= 之前已经入栈的运算符, 则需先计算(调用eval)先前入栈的运算符再入栈当前操作符, 反之直接入栈
循环完若操作栈非空,则不断eval()直到没有操作符, 最后运算结果为num.top()

#include 
#include 
#include 
#include //三个参数key,value,类(一般省略)
#include 

using namespace std;

stack<char> op;//运算符栈
stack<int> num;//数值栈

void eval()//计算函数 【弹栈op一个c,num两个a,b: 操作数a 运算符c 操作数b  根据运算符判断运算方式 计算结果x存回数值栈 】
{
    auto b = num.top(); num.pop();//第一个操作数
    auto a = num.top(); num.pop();//第二个操作数
    auto c = op.top(); op.pop();//运算符

    int x;//存放结果
    if (c == '+') x = a + b;
    else if (c == '-') x = a - b;
    else if (c == '*') x = a * b;
    else x = a / b;
    num.push(x);//计算结果存回数值栈
}

int main()
{
    string s;//字符串
    cin >> s;

    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};//运算符优先级 (key,value),key唯一标识,value值代表运算优先级大小
    for (int i = 0; i < s.size(); i ++ )
    {
        if (isdigit(s[i]))//判断是否为数字
        {
            int j = i, x = 0;//x辅助存数值
            while (j < s.size() && isdigit(s[j]))
                x = x * 10 + s[j ++ ] - '0';//字符串转数字
            num.push(x);//计算完放入num数字栈
            i = j - 1;
        } //括号特殊,遇到左括号直接入栈,遇到右括号计算括号里面的
        else if (s[i] == '(') op.push(s[i]);//读到左括号直接放入操作栈op
        else if (s[i] == ')')
        {   //读到右括号,运算调用eval函数计算到括号之间的表达式, 不断弹栈直到左括号结束, 再弹出左括号
            while (op.top() != '(') eval();
            op.pop(); //最后把'('出栈
        }
        else    
        {   //遇到普通运算符, 若栈非空且当前优先级 <= 栈中优先级 : 先把栈中优先级高的运算符计算完, 再把操作符入栈
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[s[i]]) eval(); 
            op.push(s[i]);//当前操作符入栈
        }
    }

    while (op.size()) eval();//最后运算到没有操作符,得出表达式运算结果由eval存回num栈顶
    cout << num.top() << endl;//调用栈顶

    return 0;
}

调试大法

void out(int x)
{
    cout << x << " ";
}

// out(x);
// out(num.top()); 

中缀表达式转后缀表达式
eval改变为输出——大部分代码同于表达式求值
(改变顺序而不是计算——提取数字部分和操作符部分按规则排序)

#include 
#include 
#include 
#include  //三个参数key,value,类(一般省略)
#include 

using namespace std;

stack<char> op;

void eval()
{
    auto c = op.top(); op.pop();
    cout << c << ' ';
}

int main()
{
    string s;
    cin >> s;

    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
    for (int i = 0; i < s.size(); i ++ )
    {
        if (isdigit(s[i]))
        {
            int j = i, x = 0;
            while (j < s.size() && isdigit(s[j])) //不满足时j指向非数字的首位
                x = x * 10 + s[j ++ ] - '0'; 
            cout << x << ' ';
            i = j - 1; //最后i++后变为j指向非数字的首位
        }
        else if (s[i] == '(') op.push(s[i]);
        else if (s[i] == ')')
        {
            while (op.top() != '(') eval();
            op.pop();
        }
        else
        {
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[s[i]])
                eval();
            op.push(s[i]);
        }
    }

    while (op.size()) eval();

    return 0;
}

AcWing 829. 模拟队列

实现一个队列,队列初始为空,支持四种操作:

push x – 向队尾插入一个数 x;
pop – 从队头弹出一个数;
empty – 判断队列是否为空;
query – 查询队头元素。
现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式
第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。

输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。

数据范围
1≤M≤100000,
1≤x≤ 1 0 9 10^9 109,
所有操作保证合法。

输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4

hh = 0, tt = -1;

#include 

using namespace std;

const int N = 100010;

int m;//模板初始值:hh = 0, tt = -1
int q[N], hh, tt = -1;//hh <= tt 能取等号的主要原因是因为tt初值为 -1,hh初值为 0,相等时表示队列中只有一个元素

int main()
{
    cin >> m;

    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            q[ ++ tt] = x;//入队
        }
        else if (op == "pop") hh ++ ;//出队
        else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;//不为空 : hh <= tt : 初始hh = 0, tt = -1
        else cout << q[hh] << endl;//取队头
    }

    return 0;
}

尝试初始hh = 0, tt = 0;

#include 

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt;//初始hh = 0, tt = 0 , 则 hh == tt 为空   hh < tt 时不为空

int main()
{
    cin >> m;

    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            q[ ++ tt] = x;//入队
        }
        else if (op == "pop") hh ++ ;//出队
        else if (op == "empty") cout << (hh < tt ? "NO" : "YES") << endl;//不为空 : hh <= tt : 初始hh = 0, tt = -1
        else cout << q[hh + 1] << endl;//取队头[此时队头下标为hh+1]
    }

    return 0;
}

AcWing 830. 单调栈

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式
第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围
1≤N≤ 1 0 5 10^5 105
1≤数列中元素≤ 1 0 9 10^9 109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

栈-先进后出 : 只能在栈顶取元素

单调栈处理问题:找左边第一个小的数 : 【原序列不单调】但可以维护单调性:等效单调栈
逆序的元素没有用【不会被选择】(有更优解,更近的小的数)
因此可以删除值逆序的元素(值相等也删除)【维护严格单调

证明:

**逆序不会被选择:值 :a[x] > a[y] , 但下标 : a x < a y a_x < a_y ax<ay
若z选择左边更小,若a[z] > a[x] 则可选逆序元素x,但同时也可以选更小的数y,
又因为y更靠近后边的z,应该优先选y,即逆序元素a[x]不可能被选择为最近的更小数 **

#include 

using namespace std;

const int N = 100010;

int stk[N], tt;//tt:栈顶指针

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;//逆序的点全部删去, 出栈
        if (!tt) printf("-1 ");//全部出栈,说明左边没有比当前数x更小的数 【注意不能用puts("-1 ")会换行】    
        else printf("%d ", stk[tt]);//找到输出
        stk[ ++ tt] = x;//删除逆序元素后, x入栈
    }

    return 0;
}
后进先出顺序: for(int i = tt; i >= 1; i --) cout << stk[i] << " ";
先进先出顺序: for(int i = 1; i <= tt; i ++) cout << stk[i] << " ";    

AcWing 154. 滑动窗口

给定一个大小为 n≤ 1 0 6 10^6 106 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式
输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。

第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式
输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7

同kmp一样劝退[多亏y总和佬们]

海绵宝宝Hasityの图

算法基础课【合集1】_第4张图片
算法基础课【合集1】_第5张图片
算法基础课【合集1】_第6张图片

先用普通队列 :先暴力, 再看能否删去非答案元素, 再想是否用单调性-二分查找优化
模拟队列 hh = 0, tt = -1, 出队hh++, 队列不空 hh <= tt 【看做双端队列deque :允许队尾出队 tt–】
队头 <— (从队尾不断入队) [不超过边界] : 整个模拟队列判断过程下标不断向右增大移动

简记:①判断队头是否已经滑出窗口(在边界内hh++) ②删除所有逆序对(tt--) ③最小的入队(++tt) **
④i到达初始窗口末尾(窗口大小k,下标k-1)开始输出,(维护
严格单调**递增队列-队头最小) : 下标取值a[q[hh]]

#include 

using namespace std;

const int N = 1000010;

int a[N], q[N];

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

    int hh = 0, tt = -1; //队列存窗口首元素在第i位时, 窗口内的最小元素的[坐标] : 取值则套数组: a[q[hh]]
    for (int i = 0; i < n; i ++ )
    {   //判断对头是否已经滑出窗口(超出则队头不移动) :每次只滑动一位(用if判断一次, 不用while)     窗口坐标范围[i - k + 1, i]   
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ; //队列不为空 && 当前滑动窗口首元素坐标i - k + 1 > 队头坐标, 说明非最小:出队删除   
        //删除所有逆序对【非最小不会作为答案】 (等于则有相同值,出队也不影响值)-严格单调
        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ; //(维护单调队列, 把前面大的元素出队,非答案)    hh再接下去几次判断中仍用到
        q[ ++ tt] = i;  //最小的入队   
        //题目要求输出前k个【当达到i第一个窗口末尾下标时才会开始输出】
        if (k - 1 <= i) printf("%d ", a[q[hh]]); //维护单调递增的队列, 最小值则为队头:注意下标取值【a[q[[hh]]】
    }

    puts("");

    hh = 0, tt = -1;//初始队列【记得重置】
    for (int i = 0; i < n; i ++ ) //每轮出队一个[边界判断] , 寻找答案, ++tt 入队[下标]
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ; 
        // 与求最小不同 , 仅为 a[q[tt]] <= a[i]删除
        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ; //求窗口内最大元素, 只需维护单调递减队列, 把窗口内小的元素删除, 非答案 
        q[ ++ tt] = i; //把剩下的最大元素下标存入队列
            
        if (k - 1 <= i) printf("%d ", a[q[hh]]);  //维护单调递减的队列, 最大值则为队头
    }

    puts("");

    return 0;
}

超多细节

上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
队列中存的是原数组的下标,取值时要再套一层,a[q[]];
算最大值前注意将hh和tt重置;
此题用cout会超时,只能用printf;
hh从0开始,数组下标也要从0开始。

另一种理解

#include
using namespace std;
const int N = 1e6 + 10;
int n, k, q[N], a[N];//q[N]存的是数组下标
int main()
{
    int tt = -1, hh=0;//hh队列头 tt队列尾
    cin.tie(0);
    ios::sync_with_stdio(false);
    cin>>n>>k;
    for(int i = 0; i <n; i ++) cin>>a[i];
    for(int i = 0; i < n; i ++)
    {
        //维持滑动窗口的大小
        //当队列不为空(hh <= tt) 且 当当前滑动窗口的大小(i - q[hh] + 1)>我们设定的
        //滑动窗口的大小(k),队列弹出队列头元素以维持滑动窗口的大小
        if(hh <= tt && k < i - q[hh] + 1) hh ++;
        //构造单调递增队列
        //当队列不为空(hh <= tt) 且 当队列队尾元素>=当前元素(a[i])时,那么队尾元素
        //就一定不是当前窗口最小值,删去队尾元素,加入当前元素(q[ ++ tt] = i)
        while(hh <= tt && a[q[tt]] >= a[i]) tt --;
        q[ ++ tt] = i;
        if(i + 1 >= k) printf("%d ", a[q[hh]]);
    }
    puts("");
    hh = 0,tt = -1;
    for(int i = 0; i < n; i ++)
    {
        if(hh <= tt && k < i - q[hh] + 1) hh ++;
        while(hh <= tt && a[q[tt]] <= a[i]) tt --;
        q[ ++ tt] = i;
        if(i + 1 >= k ) printf("%d ", a[q[hh]]);
    }
    return 0;
}

deque双端队列

#include 
using namespace std;

const int MAXN = 1e6 + 10;
int a[MAXN];
deque < int > q;//q存放编号 

int main()
{
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    q.clear();
    //单调递增 求最小值
    for (int i = 1; i <= n; i++)
    {
        while (!q.empty() && a[q.back()] >= a[i]) q.pop_back();
        q.push_back(i);
        while (!q.empty() && i - k >= q.front()) q.pop_front();
        if (i >= k) cout << a[q.front()] << " ";
    }
    cout << endl;
    q.clear();
    for (int i = 1; i <= n; i++)
    {
        while (!q.empty() && a[q.back()] <= a[i]) q.pop_back();
        q.push_back(i);
        while (!q.empty() && i - k >= q.front()) q.pop_front();
        if (i >= k) cout << a[q.front()] << " ";
    }
    cout << endl;
    return 0;
}

AcWing 831. KMP字符串

给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P 在字符串 S 中多次作为子串出现。

求出模式串 P 在字符串 S 中所有出现的位置的起始下标。

输入格式
第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

数据范围
1≤N≤ 1 0 5 10^5 105
1≤M≤ 1 0 6 10^6 106
输入样例:
3
aba
5
ababa
输出样例:
0 2

kmp字符串下标从1开始

#include 

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
char p[N], s[M];//p模式串十万 , s主串百万
int ne[N];

int main()
{
    scanf("%d%s", &n, p + 1);//从1开始
    scanf("%d%s", &m, s + 1);

    for (int i = 2, j = 0; i <= n; i ++ )//模式串自身匹配,构造ne[i] 
    {	
        while (j && p[i] != p[j + 1]) j = ne[j];//j为0就到起点,不能回溯了 ,不相同时j=ne[j]回溯
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;//相同前后缀的长度
    }

    for (int i = 1, j = 0; i <= m; i ++ )//主串长度m与模式串匹配【简记:判读语句中变化仅前部分p换成s】
    {
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n) printf("%d ", i - n);//输出所有出现位置的起始下标(从0开始,不用i-n+1)
    }
    return 0;
}

AcWing 835. Trie字符串统计

维护一个字符串集合,支持两种操作:

I x 向集合中插入一个字符串 x;
Q x 询问一个字符串在集合中出现了多少次。
共有 N 个操作,所有输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

输入格式
第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。

输出格式
对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围
1≤N≤ 2 ∗ 1 0 4 2∗10^4 2104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1

Trie字符串统计 - 模板

解决问题类型-输入n个字符串:
①统计相同字符串数量
②字符串查找

核心操作:
①插入:p指针从根开始, 遍历str到最后字符位置统计str模式串个数cnt[p]++ : 注意++ idx
②查询:代码类似插入,不做修改,看是否能遍历完str(即能找到模式串str对应分支)

语法:
char op[2]; '\0’需多开一个
*op == op[0] , *(op + 1) == op[1]

son[N][26] 存每个字符idx的后继字符下标++idx【每个后继0-25共26个分支】

#include 

using namespace std;

const int N = 100010;
//son[N][26] :开N : 因为最多会有N个不同的单词,有N个分支【看图理解Trie树】
int son[N][26], cnt[N], idx;//根,空节点均为0 
char str[N];

void insert(char *str)//插入   :char str[]
{
    int p = 0;//p指针(指向每个字母下标), 从根开始->根的子节点【str的首字母】
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';//映射成 0~25
        if (!son[p][u]) son[p][u] = ++ idx;//没有此路(没有此模式串,构造模式串,消耗下标:注意idx已被使用, 先++idx)
        p = son[p][u];//递归:【继续判断下一个单词的位置,没路就开路】 
    }
    cnt[p] ++ ;//遍历完在最后一个字符的下标p ++
}

int query(char *str)//询问
{
    int p = 0;//p指针:str[i]下标
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0; //不存在此模式串,返回0次
        p = son[p][u];
    }
    return cnt[p];//若存在,则返回出现次数
}

int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        char op[2];//还要留个'\0'
        scanf("%s%s", op, str);
        if (op[0] == 'I') insert(str);//op[0]等效*op  ==> op[1] == *(op + 1)
        else printf("%d\n", query(str));
    }

    return 0;
}

mycode2022-引用&简化代码

#include 

using namespace std;

const int N = 100010;
//son[N][26] :开N : 因为最多会有N个不同的单词,有N个分支【看图理解Trie树】
int son[N][26], cnt[N], idx;//根,空节点均为0 
char str[N];

void insert(char str[])
{
    int p = 0;
    for(int i = 0; str[i] ; i++)
    {
        int &s = son[p][str[i] - 'a']; //每层对应分支映射0-25
        if(!s) s = ++ idx;
        p = s;
    }
    
    cnt[p] ++;
}

int query(char str[])
{
    int p = 0;
    for(int i = 0; str[i]; i++)
    {
        int &s = son[p][str[i] - 'a'];
        if(!s) return 0;
        p = s;
    }
    return cnt[p];
}


int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        char op[2];//还要留个'\0'
        scanf("%s%s", op, str);
        if (*op == 'I') insert(str);//op[0]等效*op  ==> op[1] == *(op + 1)
        else printf("%d\n", query(str));
    }

    return 0;
}

理解图
算法基础课【合集1】_第7张图片

AcWing 143. 最大异或对

在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?

输入格式
第一行输入一个整数 N。

第二行输入 N 个整数 A1~AN。

输出格式
输出一个整数表示答案。

数据范围
1≤N≤ 1 0 5 10^5 105,
0≤Ai< 2 31 2^{31} 231
输入样例:
3
1 2 3
输出样例:
3

暴力:两两异或取max O ( n 2 ) O(n^2) O(n2) 超时
A[i]固定, 优化第二层选取A[1]~A[N](不包括A[i])中选取与A[i]异或max

构造tire树 :尽量选择不同的 是0往1走, 是1往0走->这样异或值才最大
注意二进制数遍历每一位【int4字节1位符号位, 后31位表示数值:异或值不管符号位: 即枚举31位, 若最高位为下标30, 则最低位下标为0】:二进制数枚举每位 for(int i = 30; i >= 0; i--)

字典树用法:①存储、查找字符串集合存储、查找二进制数字[最高位为根]
思路:将每个数以二进制方式存入字典树
A[i]二进制数从最高位】开始匹配:走到终点代表选择了A[1]~A[N]中的一个 (肯定不会选到自己(与自己值异或值最小 = 0)) 【每位尽量选择不同,使得异或值最大】
异或运算:0^1 = 1^0 = 1

别名的妙用:【简化代码】int &s = son[p][x >> i & 1];

#include 
#include 

using namespace std;

const int N = 100010, M = 3100010; //节点个数 N个数 * 每个数二进制表示31位 , 最多 N * 31的分支  

int n;
int a[N], son[M][2], idx; //二进制数每位0, 1两个分支

void insert(int x)
{
    int p = 0; //注意二进制数遍历每一位【int 2^31 - 1: 即31位, 若最高位为下标0, 则最低位下标为30】从最高位起【尽量选择不同异或值最大】
    for (int i = 30; i >= 0; i -- )  //i >= 0可以写成 ~i : 因为停止时i = -1:补码二进制表示为全1
    {   //int u = x >> i & 1; 加上则为模板:son[p][u]
        int &s = son[p][x >> i & 1]; //别名的妙用:【简化代码】
        if (!s) s = ++ idx; //没有此前缀(路), 创建前缀(路)
        p = s; //指针移动
    }
}

int search(int x) //返回A[i] = x 异或对的值:最大值
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i -- ) //【二进制数下标必从0开始】
    {
        int s = x >> i & 1; //下标从0开始,取x二进制数的第i位
        if (son[p][!s]) //(0,1)与(1,0): (s ^ !s)配对异或结果为1 :此结果分支存在
        {
            res += 1 << i; //加上此位的二进制值转十进制值
            p = son[p][!s];
        }
        else p = son[p][s]; //没有分支只能选择异或值为0的
    }
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) 
    {
        scanf("%d", &a[i]);
        insert(a[i]); //构造字典树
    }

    int res = 0; //查找A[i]在字典树中选取不包括自身的A[1]~A[N]的异或值MAX
    for (int i = 0; i < n; i ++ ) res = max(res, search(a[i])); 

    printf("%d\n", res);

    return 0;
}

算法基础课【合集1】_第8张图片

暴力枚举-(a[1] ~ a[N]两两组合)

int res = 0;
for (int i = 0; i < n; i ++ ) 
    for (int j = 0; j < i; j ++ ) //注意j枚举到j < i即可 :后面就重复了
        res = max(res, a[i] ^ a[j]);

AcWing 836. 合并集合

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。

输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围
1≤n,m≤ 1 0 5 10^5 105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes

基础课-路径压缩
经典优化:
路径压缩find(x): 元素都指向根,下次查找祖先只需O(1)
按值合并merge(a, b): 多维护一个数组:树高(层数)或者集合个数 : 小的合并到大的

算法题用路径压缩足够优化

#include 

using namespace std;

const int N = 100010;

int p[N];

int find(int x)//返回x的祖宗节点 : 路径压缩  (过程中直接指向根节点,存的值改为根节点下标,加快之后的查找)  
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    while (m -- )
    {
        char op[2]; //末尾需放'\0'标志结束
        int a, b;
        scanf("%s%d%d", op, &a, &b); //*op取首元素
        if (*op == 'M') p[find(a)] = find(b);//合并  (简化判断:不用管是否在一个集合)
        else //查询
        {
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }

    return 0;
}

mycode2022-化简

#include 

using namespace std;

const int N = 100010;

int p[N];

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    while (m -- )
    {
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        a = find(a), b = find(b); //先提取集合编号(祖先)
        if (*op == 'M') p[a] = b; //合并
        else //查询
        {
            if (a == b) puts("Yes");
            else puts("No");
        }
    }

    return 0;
}

考研辅导版-按秩合并

经典优化:
①路径压缩 : find() 元素都指向根,下次查找祖先只需O(1)
②按秩合并【优化不明显一般不用,但考研因为教材有最好写上】
【按秩合并:多维护一个数组:高度或元素个数 : 小的并入大的集合】

#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n, m;
int p[N], r[N];//r[x]即记录x所在集合的根节点的高度 

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

//[merge按秩合并] : 高度越小查找越优化(层数越少, 路径越短)     :将矮的树加到高的树
void merge(int a, int b)//合并a,b到同一个集合【比较祖先放到集合元素数量较多的】
{
    a = find(a), b = find(b);//简写代码
    if (a == b) return;//在同一个集合,直接退出
    
    if (r[a] > r[b]) p[b] = a;//a所在的集合元素更多,b放到a所在的集合 b父结点为a : p[b] = a  [更高合并在子节点,高度不加]
    else //r[a] <= r[b]
    {
        p[a] = b; //b所在的集合元素更多,a放到b所在的集合 a父结点为b : p[a] = b 
        if (r[a] == r[b]) r[b] ++ ;//如果高度相同,则因为插入在子节点下面, 元素集合的树形式对应高度会加1
    }
}

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

    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        r[i] = 1;//root根节点高度初始化1
    }

    while (m -- )
    {
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        if (*op == 'Q')
        {
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else merge(a, b);//按秩合并
    }

    return 0;
}

AcWing 837. 连通块中点的数量

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
Q2 a,询问点 a 所在连通块中点的数量;
输入格式
第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。

输出格式
对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No。

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围
1≤n,m≤ 1 0 5 10^5 105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3

并查集+维护集合个数
维护集合元素个数:不用size会冲突, 用cnt

#include 

using namespace std;

const int N = 100010;

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

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

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

    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;//初始化并查集【每个元素自己为根】
        cnt[i] = 1;//以点i为根的集合有多少个元素
    }

    while (m -- )
    {
        string op;
        int a, b;
        cin >> op;

        if (op == "C")//连接无向边 a <--> b
        {
            scanf("%d%d", &a, &b);
            a = find(a), b = find(b);//【总之必须先提出,不仅仅简化,更是逻辑】!!!!!    
            if (a != b)//根不同, 不在同一个集合
            {
                p[a] = b;//a集合的根的父结点为b集合的根
                cnt[b] += cnt[a];//【合并到b所在集合 :b所在集合元素个数 += a所在集合元素】
            }
           
        }
        else if (op == "Q1")//判断点a, b是否在同一个集合(是否连通)
        {
            scanf("%d%d", &a, &b);
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else//查询点a所在集合(连通块)的元素个数
        {
            scanf("%d", &a);//输入点下标
            printf("%d\n", cnt[find(a)]);//查询点对应集合的元素个数
        }
    }

    return 0;
}

评论区大佬

大家看y总这段代码时要注意:
在C操作时,y总先把a,b的根结点取出来了:a = find(a), b = find(b);
因此接下来是先将集合a接到集合b下再把a的连通块大小加到b上,
还是先把a的连通块大小加到b上再操作集合都是可以的,
如果大家没有提前一步的处理,就必须要先加连通块大小再操作集合,
否则操作完集合后,a和b的根结点将会重叠,导致输出错误!

作者:Shadow

#include
#define read(x) scanf("%d",&x) //有趣~
using namespace std;
const int N = 1e5+5;
int n,m,a,b,fa[N], size[N];
string act;

void init() {
    for (int i=1; i<=n; i++) {
        fa[i] = i;
        size[i] = 1;
    }
}

int find(int x) {
    if(fa[x]==x) return x;
    else return fa[x] = find(fa[x]);
}

void merge(int a,int b) {
    int x = find(a);
    int y = find(b);
    fa[x] = y;
    size[y] += size[x];
}

bool ask(int a,int b) {
    return find(a)==find(b);
}

int main() {
    read(n),read(m);
    init();
    while(m--) {
        cin>>act;
        if(act=="C") {
            read(a),read(b);
            if(!ask(a,b)) merge(a,b);
        } else if(act=="Q1") {
            read(a),read(b);
            ask(a,b) ? printf("Yes\n") : printf("No\n");
        } else {
            read(a);
            printf("%d\n",size[find(a)]);
        }
    }   
    return 0;
}

AcWing 240. 食物链

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

当前的话与前面的某些真的话冲突,就是假话;
当前的话中 X 或 Y 比 N 大,就是假话;
当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式
第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式
只有一个整数,表示假话的数目。

数据范围
1≤N≤50000,
0≤K≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3

并查集 + 维护到根节点的距离 :
吃第一代的为第二代, 吃第二代的第三代… 【所有代的种类就可以%3分类
模3分类余1 :可以吃根节点, **余2 **: 可以被根节点吃 , 余0:与根节点同类
算法基础课【合集1】_第9张图片

一代吃一代循环【题目仅分三类】

算法基础课【合集1】_第10张图片

#include 

using namespace std;

const int N = 50010;

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

int find(int x)
{
    if (p[x] != x)
    {//如果先让p[x]等于根节点, 则d[x]加的距离就为根节点的距离【错误】
        int t = find(p[x]);  //先不能改变p[x] 用t先存-重要(find([p[x]])过程中会改变d!!!)
        d[x] += d[p[x]]; //距离:x -> p[x]  + p[x] -> 根节点 【有点小问题】
        p[x] = t;
    }
    return p[x];
}


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

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    int res = 0; //假话数量
    while (m -- )
    {
        int t, x, y; //t = 1 :认为x和y是同类, t = 2 则认为x吃y
        scanf("%d%d%d", &t, &x, &y);

        if (x > n || y > n) res ++ ; //超过下标数量, 假话
        else
        {
            int px = find(x), py = find(y); //存x, y根节点编号
            if (t == 1)//判断是否同类
            {
                if (px == py && (d[x] - d[y]) % 3) res ++ ; //根节点不同 && 不同余 : 假话 res++ 
                else if (px != py) //不在统一集合, 但同余: 加入统一集合 同时更新距离
                {
                    p[px] = py;
                    d[px] = d[y] - d[x]; 
                }
            }
            else
            {
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ; //x吃y说明 d[x - 1] 与 d[y] 同余 3 , 不同余为0 则为假话res++ 
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x]; //需要推导- 再看几遍hh 
                }
            }
        }
    }

    printf("%d\n", res);

    return 0;
}

AcWing 838. 堆排序

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

输入格式
第一行包含整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围
1≤m≤n≤ 1 0 5 10^5 105
1≤数列中元素≤ 1 0 9 10^9 109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3

**【下标1开始】 按大根堆:堆顶最大 **
(出堆元素排最后从小到大排序,大的放最后) 每轮放一个最大元素

函数版-详细注释

#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n;
int q[N], sz, w[N];


void down(int u)//递归 
{
    int t = u;
    if (u * 2 <= sz && q[u * 2] > q[t]) t = u * 2; //如果左儿子在界内且大于根节点 
    if (u * 2 + 1 <= sz && q[u * 2 + 1] > q[t]) t = u * 2 + 1;//如果右儿子在界内且大于根节点

    if (u != t)  
    {
        swap(q[u], q[t]);
        down(t);//递归往下调整好 
    }
}

//大根堆:满足根节点的值大于等于子节点的二叉树,子节点的分支也均满足。

void heap_sort()  // 堆排序,下标一定要从1开始!  main函数的读入和输出也从1开始
{
    sz = n;//维护size,堆有多少个元素   【y总翻车,sz没有开全局更新】
    for (int i = n / 2; i; i -- ) down(i); //最后一个节点的父节点开始 
	//i为循环次数而已 ,多一次sz = 1 自己与自己交换也不影响  
    for (int i = 0; i < n - 1; i ++ )//出堆共n-1次,剩下一个最小在正确位置上
    {
        swap(q[1], q[sz]);//【大根堆,堆顶最大:从小到大】堆顶与最后结点交换,放在编号最大的位置,出堆 
        sz -- ;//出堆 【只剩下sz个元素需要排序】 
        down(1);//从堆顶往下调整 
    }
}

int main()//【下标从1开始读入!!!】
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &q[i]);
    heap_sort(); //下标一定要从1开始
    for (int i = 1; i <= n; i ++ ) printf("%d ", q[i]);
    return 0;
}

简化注释版

#include
#include //scanf 和 printf

using namespace std;

const int N = 100010;

int n, m;
int q[N];
int sz;//维护个数size


void down(int u)//按大根堆
{
    int t = u;
    if(u * 2 <= sz && q[u * 2] > q[t]) t = u * 2;//改变为小根堆 ==> q[u * 2] < q[t]  ==>从大到小
    if(u * 2 + 1 <= sz && q[u * 2 + 1] > q[t]) t = u * 2 + 1;
    if(u != t)
    {
        swap(q[u], q[t]);
        down(t);
    }
}

void heap_sort()
{
    sz = n;//排序个数size
    for(int i = n / 2; i ; i--) down(i);
    for(int i = 0; i < n - 1; i++)
    {
        swap(q[1], q[sz]);//可合并==> swap(q[1], q[sz --]); down(1);
        sz --;
        down(1);
    }
}

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

直接输出版

#include 
#include 

using namespace std;

const int N = 100010;

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

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 (u != t)
    {
        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;

    for (int i = n / 2; i; i -- ) down(i);

    while (m -- )
    {
        printf("%d ", h[1]);
        h[1] = h[cnt -- ];
        down(1);
    }

    puts("");

    return 0;
}

AcWing 839. 模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

1.I x,插入一个数 x;
2.PM,输出当前集合中的最小值;
3.DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
4.D k,删除第 k 个插入的数;
5.C k x,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。

输入格式
第一行包含整数 N。

接下来 N 行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。

输出格式
对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围
1≤N≤ 1 0 5 10^5 105
1 0 9 10^9 109≤x≤ 1 0 9 10^9 109
数据保证合法。

输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6

在这里插入代码片

AcWing 840. 模拟散列表

维护一个集合,支持如下几种操作:

1.I x,插入一个数 x;
2.Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式
第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。

输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围
1≤N≤ 1 0 5 10^5 105
1 0 9 10^9 109≤x≤ 1 0 9 10^9 109
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No

hash表处理问题 :插入一个数 x, 询问数 x 是否在集合中出现过优化离散数据减少空间消耗
闭散列方法(开放寻址法)[代码更短]

因为C++负数取模仍为负数, 所以hash映射值 = (x % N + N) % N 【无论 x 正负均映射到(0 ~ N-1)】
模拟是为了能理解其实现原理STL有现成哈希的unordered_map heap;

(考研模拟, 竞赛用STL哈希表)

#include 
#include 
#include 

using namespace std;

const int N = 200003, null = 0x3f3f3f3f; //【N取第一个大于2e5的质数】 

int n;
int h[N]; //存x的映射值

int find(int x) //开放寻址法查找
{
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x) //不为空且值相等
        t = (t + 1) % N;
    return t;//找不到x值,最终落在未赋值的0
}

int main()
{
    memset(h, 0x3f, sizeof h);
    scanf("%d", &n);
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        if (*op == 'I') h[find(x)] = x;//find找到位置且赋值
        else
        {
            if (h[find(x)] == null) puts("No");
            else puts("Yes");
        }
    }

    return 0;
}

开散列方法(拉链法)

#include 
#include 
#include 

using namespace std;

const int N = 200003;

int n;
int h[N], e[N], ne[N], idx;//邻接表(h头结点数组, 每个h[i]都是一条链表)

bool find(int x)
{
    int t = (x % N + N) % N;
    for (int i = h[t]; ~i; i = ne[i])//选取hash对应的头结点h[t]:遍历邻接表
        if (e[i] == x)
            return true;
    return false;
}

void add(int a,int b )//链表头插法 [可记为:位置下标a插入b值] : 添加一条边 a->b 
{
	e[idx] = b , ne[idx] = h[a] , h[a] = idx ++;
}

void insert(int x)
{
	if(find(x)) return ;
	int t = (x % N  + N) % N ; //负数整数都转化成为正数 
	add(t,x);//映射下标t,插入x值
}
int main()
{
	memset(h , -1 , sizeof h);//二进制全为1  ,遍历邻接表 i = h[t]; ~i 按位取反二进制每位全部为0
	scanf("%d",&n);
	while(n --)
	{
		char op[2];
		int x;
		scanf("%s%d",op,&x);
		if(*op == 'I' ) insert(x);//*op == op[0]
		else 
		{
			if(find(x)) puts("Yes");
			else puts("No");
		}
	}
	
	return 0;
}

AcWing 841. 字符串哈希

给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式
第一行包含整数 n 和 m,表示字符串长度和询问次数。

第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。

接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 1 开始编号。

输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。

每个结果占一行。

数据范围
1≤n,m≤ 1 0 5 10^5 105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes

字符串哈希-模板
可替代kmp
思想:不同区间的元素hash值用公式计算若相等则说明不同区间的字符串匹配【相等】
解决问题:【判断区间[L1, R1] 与 [L2, R2] 的字符串是否匹配】

P进制的数值 (P取经验值减少冲突)
转化成十进制 (预处理p[i])
注意:不能把字符映射成数值0 : 如A为0 则 AA也为0,无法区分

#include 
#include 

using namespace std;

typedef unsigned long long ULL;//[0,2^64 - 1] : ULL存储数值 : 等效 % 2^64

const int N = 100010, P = 131;//大P = 131或1331【经验值】减少冲突

int n, m;
char str[N];
ULL h[N], p[N];//h[i]:[0,i]区间hash值  , p[i] : 经验值大P的i次方
//证明略
ULL get(int l, int r)//计算区间[l, r]的hash值 % ULL范围(等效2^64)
{
    return h[r] - h[l - 1] * p[r - l + 1];//前缀和 + 高低位的位移
}

int main()
{
    scanf("%d%d", &n, &m);
    scanf("%s", str + 1);//h数组初始化用下标i-1:str从1开始

    p[0] = 1;//乘积底数1 :大写P^0 = 1
    for (int i = 1; i <= n; i ++ ) // 【注意每次乘大写P】
    {
        h[i] = h[i - 1] * P + str[i];//初始化计算[0,i]的区间hash值
        p[i] = p[i - 1] * P;//预处理p数组 p^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;
}

评论区高手证明


/*
本次重要知识点: 类似前缀和的做法,理解什么叫字符串前缀.(不懂可以去看看我在KMP下面的留言)

然后本节课难点再于 y总给出的公式: h[R] - h[L - 1] X P ^ (R - L + 1)

注意听视频11:20开始, 我们在预处理h[i] 数组的时候,是把左边看成高位,右边看成低位(这与我们的习惯是相同的)
下面给出例子,计算[4,5]之间"de" 的哈希值   高位   a       b   c   d    e  低位  
                                            a        b      c  
                        这是数组的下标        1      2   3    L    R   
                        这里是P进制下位权    (p^4)  p^3  p^2   p^1   1

   我们在前缀和那节课已经学过了,怎么计算区间的部分和.  h[R] - h[L - 1].
   仔细一看,不对劲,位置对不上. 因此我们将字符串 "左移",为他们补上位权,这样子就能做到一一对应
                                    高位    a       b   c   d    e  低位  
                                            a      b   c   '\0' '\0'
                        这是数组的下标        1      2   3    L    R   
                        这里是P进制下位权    (p^4)  p^3  p^2   p^1   1

    为了方便理解,我用"\0"表示无意义字符. 这个时候就能计算了对吧?
    那位移是多少呢?
    就是 R - L + 1,在本例中, 5 - 3 + 1 = 2,左移两位. 补齐低位. 
    因此 h[R] - h[L - 1] X P ^ (R - L + 1)  // X是大写字母,只是这样方便观察.

*/

搜索与图论

算法基础课【合集1】_第11张图片

AcWing 842. 排列数字

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式
共一行,包含一个整数 n。

输出格式
按字典序输出所有排列方案,每个方案占一行。

数据范围
1≤n≤7
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

dfs模板题 - 全排列

#include

using namespace std;

const int N = 10;
int path[N];
bool st[N];
int n;

void dfs(int u)
{
    if(u == n)
    {
        for(int i = 0; i < n; i++) printf("%d ",path[i]);
        puts("");
    }
    
    for(int i = 1; i <= n; i++)
    {
        if(!st[i])
        {
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            st[i] = false;
        }
    }
}

int main()
{
    scanf("%d", &n);
    dfs(0);
    
    return 0;
}

next_primutation(begin[第一个元素地址], end[最后一个元素地址] )
next_permutation()是按字典序依次排列的,当排列到最大的值是就会返回false.

next_permutation函数按字典序生成给定序列的下一个较大的排列;
而prev_permutation则相反,按字典序生成给定序列的上一个较小的排列[逆字典序]

全排列-字典序

**algorithm库:next_permutation(q + 1, q + n + 1) 【从1开始存放】 **

#include
#include

using namespace std;

const int N = 10;

int n;
int q[N];

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

扩展:全排列-逆字典序
【初始值 :n -> 1】 + prev(q + 1, q + n + 1)

#include
#include

using namespace std;

const int N = 10;
int q[N];

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

AcWing 843. n-皇后问题

n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

在这里插入图片描述

现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式
共一行,包含整数 n。

输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。

其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。

每个方案输出完成后,输出一个空行。

注意:行末不能有多余空格。

输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围
1≤n≤9
输入样例:
4
输出样例:

.Q..
...Q
Q...
..Q.

..Q.
Q...
...Q
.Q..

DFS经典题

对角线数学函数:正对角线: y = -x + b 与 副对角线:y = x + b (b为截距:枚举)
判断截距b是否被选择:
正对角线 b == x + y == u + i
副对角线:b == (-x + y) % n 【映射[0,n-1]】 == -u + i + n : [注意:加n为了防止负数超出数组范围]

①按行枚举 - u当前行

#include 

using namespace std;

const int N = 20;

int n;//棋盘大小-n皇后
char g[N][N];//棋盘
bool col[N], dg[N], udg[N];//列 + 正对角线 + 反对角线

void dfs(int u) // u为x
{
    if (u == n)
    {
        for (int i = 0; i < n; i ++ ) puts(g[i]);//简用puts(输出一行 + 换行)
        puts("");
        return;
    }

    for (int i = 0; i < n; i ++ )//按行枚举 [u行][i列] : u为x, i为y
        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()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';

    dfs(0);

    return 0;
}

②每行-按列搜索
第二种搜索顺序(my不常用hh)

#include 

using namespace std;

const int N = 10;

int n;
bool row[N], col[N], dg[N * 2], udg[N * 2]; //注意行列都要边标记
char g[N][N];

void dfs(int x, int y, int s) //s统计已放皇后个数
{
    if (s > n) return;
    if (y == n) y = 0, x ++ ; //每行:按列搜索 (每行搜索完需换到下一行)

    if (x == n) //说明搜索到了终点(上一个y换行前(x, y)为(n - 1, n - 1):已经遍历完)
    {
        if (s == n) //如果放入个数为n, 说明成功,输出答案
        {
            for (int i = 0; i < n; i ++ ) puts(g[i]);
            puts("");
        }
        return;
    }

    g[x][y] = '.';
    dfs(x, y + 1, s); 

    if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n])
    {
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
        g[x][y] = 'Q';
        dfs(x, y + 1, s + 1); //每行:按列遍历
        g[x][y] = '.';
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
    }
}

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

    dfs(0, 0, 0);

    return 0;
}

八皇后-取第i种方案-考研版

AcWing 844. 走迷宫

给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。

数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。

输入格式
第一行包含两个整数 n 和 m。

接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。

输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围
1≤n,m≤100
输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8

BFS基础题-迷宫最小步数
PII q[N * N]; 空间大小N * N 组坐标元素【注意大小】

#include 
#include 
#include 

#define x first//简化pair代码
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m;

int g[N][N], d[N][N]; //不用标记st【d[][] == -1 :未走过】
PII q[N  * N];//空间大小N * N 组坐标元素【注意大小】

int bfs()
{
    //模拟队列hh = 0, tt = -1    

    memset(d, -1, sizeof d);
    d[0][0] = 0;
    q[0] = {0, 0};//因为先把{0, 0}加进去了, 即q[++tt] = {0, 0} 所以初始tt = 0
    int hh = 0, tt =  0;//起点入队 tt ++ ==>此时tt = 0;
        
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};    
    
    
    while(hh <= tt)
    {
        auto t = q[hh ++];
        for(int i = 0; i < 4; i++)
        {
            int x = t.x + dx[i] , y = t.y + dy[i];
            if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) 
            {
                d[x][y] = d[t.x][t.y] + 1;
                q[++ tt] = {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;
}

2022推荐版 模拟队列初始hh = 0, tt = -1

#include
#include
#include 

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m;

int g[N][N], d[N][N]; //不用标记st【d[][] == -1 :未走过】
PII q[N  * N];//空间大小N * N 组坐标元素

int bfs()
{
    int hh = 0, tt = -1;    
    
    memset(d, -1, sizeof d);
    d[0][0] = 0;//不要后写memset覆盖d[0][0]变成-1 
    q[++ tt] = {0, 0};
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};    
    
    while(hh <= tt)
    {
        auto t = q[hh ++];
        for(int i = 0; i < 4; i++)
        {
            int x = t.x + dx[i] , y = t.y + dy[i];
            if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) 
            {
                d[x][y] = d[t.x][t.y] + 1;
                q[++ tt] = {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;
}

STL:queue容器版
封装好的队列函数 : q.size(), q.front(), q.pop(), q.push() , q.empty()
队列不为空 ;!q.empty() 或 q.size() > 0

按顺序存入, 则队列先进先出
若输出路径都是逆序存入, 想要正着输出:
加个栈,先进后出,重新输出保存路径的栈就行。

stack栈函数: s.top(), s.push(), s.pop(), s.size(), s.empty()

注意语法:queue< PII > path; 或 stack< PII > path; 不要加[N * N]

#include 
#include 
#include 
#include

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m;
int g[N][N], d[N][N];
queue<PII> path;

int bfs()
{
    queue<PII> q;

    memset(d, -1, sizeof d);
    d[0][0] = 0;
    q.push({0, 0});

    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

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

            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});
                path.push({x, y});
            }
        }
    }
    
    int x, y;
    while(path.size())//queue path : FIFO先进先出 -正序输出  【若逆序存入,则用stack path : LIFO后进先出 -正序输出】
    {
        x = path.front().x, y = path.front().y;
        path.pop();
        printf("%d %d\n", x, y);  //逆序输出路径 【正序改用栈stack path[N * N]存入】
    }

    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 845. 八数码

在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。

例如:

1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。

我们的目的是通过交换,使得网格变为如下排列(称为正确排列):

1 2 3
4 5 6
7 8 x
例如,示例中图形就可以通过让 x 先后与右、下、右三个方向的数字交换成功得到正确排列。

交换过程如下:

1 2 3   1 2 3   1 2 3   1 2 3
x 4 6   4 x 6   4 5 6   4 5 6
7 5 8   7 5 8   7 x 8   7 8 x

现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。

输入格式
输入占一行,将 3×3 的初始网格描绘出来。

例如,如果初始网格如下所示:

1 2 3 
x 4 6 
7 5 8 

则输入为:1 2 3 x 4 6 7 5 8

输出格式
输出占一行,包含一个整数,表示最少交换次数。

如果不存在解决方案,则输出 −1。

输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19

BFS
难点:状态表示:简单法用string
存储距离(步数):unordered_map d , BFS存储状态 queue q

把string恢复成 3 * 3 : 把str[i]的元素转换到对应的3 * 3九宫格矩阵中的坐标, 常用int x = k / 3, y = k % 3; (x行y列)
交换时(x, y)–> i : 需用在string (取队头t)中的下标i, 交换string (取队头t) 与 下标k('x’的下标)【只能移动'x'交换
映射回下标i = (x * 3 + y) int k = t.find('x'); 交换元素值-数组[下标] swap(t[a * 3 + b], t[k]);

#include 
#include 
#include 
#include 

using namespace std;

int bfs(string state)
{
    queue<string> q;
    unordered_map<string, int> d;

    q.push(state);
    d[state] = 0;

    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

    string end = "12345678x"; //最终结束状态
    while (q.size())
    {
        auto t = q.front();
        q.pop();

        if (t == end) return d[t];  

        int distance = d[t]; //【过程中交换t, d[t]将找不到, 先保存】
        int k = t.find('x');  //找到x的string中下标位置【只能用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[a * 3 + b], t[k]); //注意恢复到string t中的下标位置 
                if (!d.count(t)) //【判断状态是否走过】 一定是用最小步数先走到,赋值
                {
                    d[t] = distance + 1; //当前步数 = 上一个状态步数 + 1
                    q.push(t);
                }
                swap(t[a * 3 + b], t[k]);//回溯
            }
        }
    }

    return -1;
}

int main()
{
    char s[2];//读取单个字符串

    string state;
    for (int i = 0; i < 9; i ++ )//有空格的读入处理
    {
        cin >> s;
        state += *s;//拼接为state :初始状态
    }

    cout << bfs(state) << endl;//求最小步数:BFS

    return 0;
}

AcWing 846. 树的重心

给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

输入格式
第一行包含整数 n,表示树的结点数。

接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。

输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。

数据范围
1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4

dfs邻接表深搜

邻接表存有向边, 若是无向图则边数 = 有向边 * 2 (最多边数 = 最多点数 * 2)【注意开数组大小
疑点:为什么不用搜n遍, 重心是怎么确定的??
算法基础课【合集1】_第12张图片

#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010, M = N * 2; //M:无向图存双倍的有向图的边数 a -> b, b -> a  

int n;
int h[N], e[M], ne[M], idx; 
int ans = N;
bool st[N];

void add(int a, int b) //【插入邻接表中以a为头结点的单链表插入点b下标】: a -> b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
//dfs(u)返回的是以u为根的子树的节点数量 (以u为根不是整个树的根)
int dfs(int u) //能求出每个子树的结点个数, 
{
    st[u] = true; //标记遍历过

    int size = 0, sum = 0; 
    for (int i = h[u]; i != -1; i = ne[i]) //遍历邻接表中以u开头的但单链表 
    {
        int j = e[i];  // cout << "e[i] = "<< e[i] << '\n'; 等弄懂再说     
        if (st[j]) continue;    

        int s = dfs(j);  // cout << "s = "<< s << '\n';
        size = max(size, s); //size :最大子树(去掉点u后形成的连通块)结点数量的max
        sum += s;
    }
        
    size = max(size, n - sum - 1); //剩下一个根连接的部分结点数量 = n - sum - 1 (减去其余部分及重心本身)
    ans = min(ans, size);

    return sum + 1; 
}

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

    memset(h, -1, sizeof h);

    for (int i = 0; i < n - 1; i ++ ) //读入初始化-无向图
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    // dfs可以任意选数字开始:因为这个图的点均连通, 不存在孤立的点 【从任何节点出都能遍历到整个连通图】
    dfs(1); //为了AC必须从1开始【注意n取值为1 ~ 1e5, n = 1, 则只有idx = 1的一个结点】 此题idx点编号从1开始

    printf("%d\n", ans);

    return 0;
}

dfs 深搜模板

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

AcWing 847. 图中点的层次

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。

所有边的长度都是 1,点的编号为 1∼n。

请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

数据范围
1≤n,m≤ 1 0 5 10^5 105
输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1

AcWing 848. 有向图的拓扑序列

给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。

输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。

否则输出 −1。

数据范围
1≤n,m≤ 1 0 5 10^5 105
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3

图中点之间的最短距离 - BFS
邻接表知识总结

具体含义理解
h[a] : 存点a邻接边(可能多个idx,头结点存放最后一次添加到a的边 )
e[idx] : 存第idx条边的终点(从h[a]遍历取的边即为 a -> e[idx] )
ne[idx]: 此边的起点h[] (头插法中第idx条边: ne[idx] -> e[idx]);
idx : 边的编号
w[idx]: 存第idx条边的边权
add(a, b, c) : 头插法第idx条边a->b边权为c

#include
#include

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], ne[N], idx; //【注意邻接表初始化h[]头结点数组 = -1 : 表示null】
int  q[N], d[N];

void add(int a, int b) //a -> b【h[a]头插入点b】
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int bfs()
{
    int hh = 0, tt = -1;
    q[++ tt] = 1; //从第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]) //h[t]开头的链表遍历 
        {   
            int j = e[i]; //【判断当前点下标i对应边的另一端点是否走过:能否从i = idx走到另一端点(下标e[i])】
            if(d[j] == -1) //未访问过 (不能用~d[j] 【如d[j] = 1,但是取反!= 0:即访问过也会遍历-SF段错误】)
            {
                d[j] = d[t] + 1;//统计步数
                q[++ tt] = j; //入队
            }
        }
    }
    return d[n]; //返回到第n个节点的步数
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h); //注意初始化头结点 = -1 :表示null
    
    for(int i = 0; i < m; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    printf("%d\n", bfs());
    
    return 0;
}

STL版 【代码清楚但效率更慢】

#include
#include
#include

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], ne[N], idx; //【注意邻接表初始化h[]头结点数组 = -1 : 表示null】
int  d[N];

void add(int a, int b) //a -> b【h[a]头插入点b】
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int bfs()
{
    memset(d, -1, sizeof d); //初始化未访问的距离

    queue<int> q;
    d[1] = 0;
    q.push(1); //起点入队

    while (q.size()) //队列非空  !q.empty()
    {
        int t = q.front(); //取队头
        q.pop(); //出队

        for (int i = h[t]; i != -1; i = ne[i]) //遍历邻接表
        {
            int j = e[i]; //【判断下标i对应边的另一端点是否走过:能否从i = idx走到另一端点(下标e[i])】
            if (d[j] == -1) //若点未访问过
            {
                d[j] = d[t] + 1; 
                q.push(j);
            }
        }
    }
    return d[n]; //返回到第n个点的步数【距离】
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h); //注意初始化头结点 = -1 :表示null
    
    for(int i = 0; i < m; i++) 
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b); //有向图
    }
    
    printf("%d\n", bfs());
    
    return 0;
}

AcWing 849. Dijkstra求最短路 I

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n≤500,
1≤m≤ 1 0 5 10^5 105,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

朴素Dijkstra O ( n 2 ) O(n^2) O(n2)
负权边 - 不能用Dijkstra 【负权边SPFA 否则堆优化版Dijkstra】
佬的证明

#include 
#include 
#include 

using namespace std;

const int N = 510, M = 100010, INF = 0x3f3f3f3f;

int n, m;
int g[N][N], dist[N]; //邻接矩阵  存起点到第n点的最短距离
bool st[N]; //已确定的最短距离的点

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);//【求min初始正无穷】
    dist[1] = 0;//起点距离为0【点下标从1开始】
    for (int i = 0; i < n; i ++ ) //n轮:每轮确定当前未访问的能更新成最短距离的点加入集合
    {
        int t = -1; //-1为初始, 【每轮:t = 不在集合s中 且 距离最近的点】
        for (int j = 1; j <= n; j ++ ) //寻找j = (1~n)中未访问过的 且 可更新的更短距离的点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))//t为初始值(第一个点) 或 经过点j距离小于t,更新 [不够清除]
                t = j;
        st[t] = true;//标记t = j,加入点j
        for (int j = 1; j <= n; j ++ ) //加入点t后, 借助t去尝试更新所有点
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    return dist[n];//返回到n最短距离
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(g, 0x3f, sizeof g); //【邻接矩阵初始化-标记INF无边】
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c);//重边取最小 g[a][b] = c : a -> b的边权为c
    }

    int res = dijkstra();
    if (res == INF) puts("-1");//INF不可达
    else printf("%d\n", res);

    return 0;
}

林小鹿
如果是问编号a到b的最短距离该怎么改呢?
回答: 初始化时将 dist[a]=0, 返回时return dist[b]

AcWing 850. Dijkstra求最短路 II

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n,m≤1.5× 1 0 5 10^5 105,
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 1 0 9 10^9 109

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

优先队列STL堆:heap{dist[idx], idx}; + 邻接表存边【可应付稀疏图】
B F S < − − − > 队列 BFS<--->队列 BFS<>队列 邻接表知识链接

核心思想:
memset(dist, 0x3f, sizeof dist); dist[1] = 0;
for(int i = 0; i < n; i++)  n个点n轮迭代 【优先队列-BFS】
    t  <--- 不在st[]中且距离最近的点(赋值给t) 【原本需要for j = 1 to n, 堆优化-优先队列每次距离最小的排在队头】O(1)
st[t] = true; 
用t去更新其他点for j = 1 to n 【堆优化版更新点ver的邻接边j = e[i]】
        
堆实现:①手写堆【代码量orz】 ②优先队列【首选】 
(优先队列小缺点:队列中元素个数可能为m个,时间复杂度mlogm 常数级近似mlogn)  
#include 
#include 
#include 
#include 

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10; //点个数

int n, m;
int h[N], w[N], e[N], ne[N], idx;  //idx可以当做边的编号
int dist[N];
bool st[N];

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

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);  //求min初始化正无穷
    dist[1] = 0; //起点距离自己为0
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1}); //起点入队 {dist[idx], idx}

    while (heap.size())
    {
        auto t = heap.top();   
        heap.pop();
        //注意点为second
        int ver = t.second, distance = t.first; //代码简写(可用d, v)  【dist[ver] == distance】
        //有冗余:已经加入集合的点跳过
        if (st[ver]) continue; //有冗余, 访问过的点【已经加入集合】:跳过
        st[ver] = true; 

        for (int i = h[ver]; i != -1; i = ne[i]) //更新【点ver的邻接边】 i为ver所有邻接边的编号idx_i
        {
            int j = e[i];//取第i条边的邻接点e[i] : j->e[i] 
            if (dist[j] > dist[ver] + w[i]) //利用新加入集合的点ver去更新其他点 + 入队 
            { //(需两步, 不能用min()一步完成)  
                dist[j] = dist[ver] + w[i];//更新距离 :若更小, 则dist = (起点->当前ver) + (ver->j)
                heap.push({dist[j], j});//入队更新{dist[idx], idx};
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1; //无法到达题意-1
    return 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\n", dijkstra());

    return 0;
}
//有空自己换个顺序写写hh  heap{didx, dist[idx]}; (目前失败不知道为什么orz)

简写版

#include 
#include 
#include 
#include 

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10; //点个数

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) //头插法 第idx条边a->b, 边权为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}); //起点入队 {dist[idx], idx}

    while (heap.size())
    {
        auto t = heap.top();   
        heap.pop();

        int d = t.first, v = t.second; 
        if (st[v]) continue; 
        st[v] = true; 

        for (int i = h[v]; i != -1; i = ne[i]) 
        {
            int j = e[i];//取第i条边的邻接点e[i] : j->e[i] 
            if (dist[j] > d + w[i]) //dist[ver] == distance(简写d)
            { 
                dist[j] = d + w[i];
                heap.push({dist[j], j}); 
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1; //无法到达题意-1
    return 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\n", dijkstra());

    return 0;
}

(BFS优先队列 pair小根堆 + 邻接表)
【正权边堆优化版Dijkstra, 负权边用SPFA】
不错呀

AcWing 853. 有边数限制的最短路

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible

注意:图中可能 存在负权回路 。

输入格式
第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

点的编号为 1∼n。

输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible。

数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3

bellman_ford - 最多经过 k 条边的最短距离 【松弛操作不用标记】
bellman_ford :可以找负环:但是时间复杂度高, 用SPFA-找负环
【求1号点到n号点最多经过 k 条边的最短距离】【k轮松弛操作

结构体struct Edge 存边及其边权 :**结构体数组edges[N]**从0开始存 a -> b 权值为 w 【运用于bellman_ford 和 kruskal】【相同写法简化记忆】

难点解释
memcpy(backup, dist, sizeof dist);
由于串联,如图:
算法基础课【合集1】_第13张图片

若限制但从2开始更新到3的最短距离变为2(但不满足最多经过k = 1条边的限制)
所以需要备份:只用上一个更新后的状态更新【而不是用不断改变的状态迭代更新】
② if (dist[n] > 0x3f3f3f3f / 2) puts(“impossible”);
举例子说明: 如 x - > y (点x和y均无法到达 为正无穷, 但是x -> y边权为minus负数, 仍会被更新 : 所以用 INF / 2判断是否可达 )

i循环k次
    j循环n次【n条边】
        auto a = edges[j].a, b =  edges[j].b, w =  edges[j].w;
        松弛操作 dist[e.b] = min(dist[e.b], dist[e.a] + e.w);  
        //min((起点 -> b), (起点 - > a) + (a -> b = w)) 
#include 
#include 
#include 

using namespace std;

const int N = 510, M = 10010; //最多N个点 M条边

struct Edge 
{
    int a, b, w;
}edges[M]; // a - > b = w   【边集合需开M】

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

void bellman_ford()
{
    memset(dist, 0x3f, sizeof  dist);
    dist[1] = 0; //起点初始化
    for(int i = 0; i < k; i++) //【限制k步】
    {   //【k轮松弛】
        memcpy(backup, dist, sizeof dist);  
        for(int j = 0; j < m; j++) //结构体存边 【从0开始】 
        {
            int a = edges[j].a , b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], backup[a] + w); //用backup上一个状态 尝试更新 【否则为迭代(用j次循环会更新j次, j - 1更新影响j)】
        }
    }
}

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

    for (int i = 0; i < m; i ++ ) //结构体存边 【从0开始】
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }

    bellman_ford();

    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible"); //注意存在负环【松弛操作会】
    else printf("%d\n", dist[n]);

    return 0;
}

另一种写法

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist); //求最短路min初始正无穷(超过最大边界即可)

    dist[1] = 0;
    for (int i = 0; i < k; i ++ ) //最多经过 k 条边 【若dist[n]没有被更新说明1经过k条边无法到达n】
    {
        memcpy(backup, dist, sizeof dist);  //必须备份上一个状态【松弛操作会改变dist】
        for (int j = 0; j < m; j ++ ) 
        {
            auto e = edges[j]; 
            dist[e.b] = min(dist[e.b], backup[e.a] + e.w);  // a - > b = w; 
        }
    }
}

统一用返回值写法【简记】

#include 
#include 
#include 

using namespace std;

const int N = 510, M = 10010; //最多N个点 M条边

struct Edge 
{
    int a, b, w;
}edges[M]; // a - > b = w   【边集合需开M】

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

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); //用backup上一个状态 尝试更新 【否则为迭代(用j次循环会更新j次, j - 1更新影响j)】
        }
    }
    return dist[n];
}//int类型函数忘记加返回值会返回系统栈esp中临时的值

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

    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        scanf("%d%d%d", &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;
}

AcWing 851. spfa求最短路

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。

数据保证不存在负权回路。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible。

数据范围
1≤n,m≤ 1 0 5 10^5 105,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2

SPFA - (BFS + 邻接表)

(SPFA是对Bellman_ford算法做优化,贝尔曼每次都要遍历所有点尝试更新,但是不是每条边都会被更新,而SPFA是去用被更新的点入队(BFS)再去更新其他点【注意不在队列中的才入队,不能重复入队(死循环)】,直到无法更新全部出队,停止)

优化思想可行性: 用变小的点更新其他点**,若被更新距离变小的点不在队列, 则加入队列**:其它路径经过变小的点(经过变小的距离)就可能会变得更短

邻接表遍历:取头结点i = h[t]开始到i == -1结束(~i循环遍历), 遍历所有点t出发的边 : t -> 其他点
类似topsort(): j = e[i] : dist[j] = min(dist[j], dist[t] + w[i])

#include 
#include 
#include 

using namespace std;

const int N = 1e5 + 10; 

int n, m; 
int h[N], w[N], e[N], ne[N], idx; 
int dist[N], q[N];
bool st[N];

void add(int a, int b, int c) //邻接表 + 边权w[idx] = c 【参数不能用w!!!与数组名称冲突】
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int spfa()
{
    int hh = 0, tt = -1;
    q[++ tt] = 1;
    st[1] = true;
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    while (hh <= tt)  
    {
        int t = q[hh ++];  //队列存放更新路径变小的点

        st[t] = false; //t出队, 此时需标记t不在队列中

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i]; //第i条边:t -> e[i]  
            if (dist[j] > dist[t] + w[i])   //用变小的点更新其它点:其它路径在经过它的基础上(经过变小的距离)就可能会变得更短
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q[++tt] = j;
                    st[j] = true; //表示j在队列中
                }
            }
        }
    }

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

    int t = spfa();

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

STL-队列

#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

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 spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        int t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return dist[n];
}

int main()
{
    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);
    }

    int t = spfa();

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

AcWing 852. spfa判断负环

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你判断图中是否存在负权回路。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。

数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes

抽屉原理-简单理解: 把n + k件物品放入n个抽屉, 则至少有1个抽屉放有大于2件物品
思想: SPFA求最短路算法的基础上维护当前最短路径经过的边数cnt[]
【若有负环 则必有边会被走过多次 更新到终点n必有cnt[i] >= n】
(注意负环判断是从任意点出发, 其中必有一点的迭代中出现cnt >= n:即先把所有点加入队列)

评论区の佬
为什么可以不用初始化dist

1 . 构造一个虚拟节点 O,单向指向所有的节点,且到所有节点距离为0;
2 . 新图是否有负环等价于原始的图。
3 . dist数组一开始为0,没有违背算法过程,可以理解为根据算法已经从O 更新到了各个节点,接下来的代码就是顺理成章。
所以dist数组从所有为0的状态开始是有对应意义的。就是先走一步。

负环建议STL
STL队列库函数-BFS使用: q.front(), q.pop(), q.push(), q.size()或q.empty()

#include 
#include 
#include 
#include 

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]; //在SPFA基础上多维护一个cnt统计此可到达的点数
bool st[N];

void add(int a, int b, int c) //邻接表 + w[idx] = c边权
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

bool spfa() //判断是否有负环
{
    queue<int> q;
    //判断负环, 不用dist, 不需要初始化 (当然初始化INF并不影响)【需想清楚的是不初始化为什么会dist能正常更新】

    for (int i = 1; i <= n; i ++ ) //题目判断是否存在负环【所有点都当做起始点尝试】【若只加入1号点则为判断:是否存在从1号点开始的负环】
    {
        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; //更新后到j最短路的经过边数 = 到t边数 + 1 (起点t -> 邻接点j = e[i])

                if (cnt[j] >= n) return true; //存在负环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

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

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

    return 0;
}

模拟队列时间缩短但空间开销大

#include 
#include 
#include 
#include 

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N]; 
bool st[N];
int q[10000000]; //空间开销很大【遍历过程同一个点会被多次使用hh++, 开1e7才能AC【STL队列会真实出队】】
bool spfa() 
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i ++ ) //题目判断是否存在负环【所有点都当做起始点尝试】【若只加入1号点则为判断:是否存在从1号点开始的负环】
    {
        st[i] = true;
        q[++ tt] = i;
    }

    while (hh <= tt)
    {
        auto t = q[hh++];
        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; //更新后最短路的边数 = (起点 -> t边数 + 1)

                if (cnt[j] >= n) return true; //存在负环
                if (!st[j])
                {
                    q[++ tt] = j;
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

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

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

    return 0;
}

AcWing 854. Floyd求最短路

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible。

数据保证图中不存在负权回路。

输入格式
第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。

输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。

数据范围
1≤n≤200,
1≤k≤ n 2 n^2 n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1

Floyd【求多源最短路】动态规划
第k - 1个状态 --> 第k个状态 :取min在(d[k - 1, i, j] 与 d[i - > k, k - > j] )
优化后状态转移方程: d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

天才美少女卡莎の图
算法基础课【合集1】_第14张图片

#include 
#include 
#include 

using namespace std;

const int N = 210, INF = 0x3f3f3f3f;

int n, m, Q;
int d[N][N]; //d[i][j] : 点i - > 点j 的最短距离

int main()
{
    scanf("%d%d%d", &n, &m, &Q);
    memset(d, 0x3f, sizeof d);//不可达INF
    for (int i = 1; i <= n; i ++ ) d[i][i] = 0;//【自己到自己距离为0】

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);//有重边选最小
    }

    for (int k = 1; k <= n; k ++ )//从1开始转移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]);//加上中间点k比较,看是否会更短

    while (Q -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        int t = d[a][b];
        if (t > INF / 2) puts("impossible");//可能负环
        else printf("%d\n", t);
    }

    return 0;
}

AcWing 858. Prim算法求最小生成树

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。

由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

数据范围
1≤n≤500,
1≤m≤ 1 0 5 10^5 105,
图中涉及边的边权的绝对值均不超过 10000。

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

prim - 最小生成树 [邻接矩阵] O ( n 2 ) O(n^2) O(n2)
最小生成树 - 选取n-1条边
与Dijkstra不同的是prim初始没有指定起点, 而是先任意选取一个点迭代n次 (但代码简记一般也选1,可选其他)

思想:首先任意选择一个点加入集合, 在非集合点中选一个能连接到集合内的任意一点且边权最小的, 对应点加入集合, 重复至所有点加入集合

prim中的dist表示的是每轮选取的点到集合中的其中一点的当前轮的最小边权
必须先累加后更新【防止负的自环更新, 最小生成树不能有环】
没有点能到达集合中的点(不连通dist为初始值 == INF) 无法构造最小生成树 - return INF

#include
#include 
#include 
using namespace std;

const int N = 510,M = 100010,INF = 0x3f3f3f3f;

int n,m;
int g[N][N],dist[N];//dist表示的是每轮加入的点到集合中点的距离【即每轮选取的最短边权】
bool st[N];

int prim()//每次从邻接点选取最小距离加入集合
{
	memset(dist , 0x3f, sizeof dist);
	dist[1] = 0;
	int res = 0;//边权和 
	for(int i = 0; i < n ; i ++ )
	{
		int t = -1; //t表示为当前边权最小的点 
		for(int j = 1; j <= n; j ++)
			if(!st[j] && (t == -1 || dist[t] > dist[j]))   //初始起点t == -1 || 选取与集合中的点相连的最短边权的对应点 - 加入集合
				t = j;
		if(dist[t] == INF) return INF;   //没有点能到达集合中的点(不连通):无法构造最小生成树 - 返回INF        
		st[t] = true;//t = j ,加入点j
		res +=	dist[t]; //res += 每轮加入集合的点对应的最短边
		for(int j = 1; j <= n; j ++) //用新加的点,更新其他路径【集合中的其他点到此点距离是否更短,更短就替换原来选的边(即可能一点连多点)】 
			dist[j] = min(dist[j] , g[t][j]); //注意这里的dist为每轮选的点到集合中的距离【即每轮选取的最短边权】
	}//必须先累加后更新【防止负的自环:会自己如g[t][t]更新, 但是最小生成树不能有环, 不能加此自环边】
	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); //a->b :value = c 
		g[a][b] = g[b][a] = min(g[a][b] , c); // 初始【无向图】 ,重边取最小
	}
	
	int res = prim();
	if(res == INF) puts("impossible");
	else printf("%d\n",res);
	
	return 0; 
}

AcWing 859. Kruskal算法求最小生成树

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。

由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

数据范围
1≤n≤ 1 0 5 10^5 105,
1≤m≤2∗ 1 0 5 10^5 105,
图中涉及边的边权的绝对值均不超过 1000。

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

AcWing 860. 染色法判定二分图

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。

输出格式
如果给定图是二分图,则输出 Yes,否则输出 No。

数据范围
1≤n,m≤ 1 0 5 10^5 105
输入样例:
4 4
1 3
1 4
2 3
2 4
输出样例:
Yes

Kruskal - (贪心 + 并查集) - O(mlogm)

边按权重从小到大排序 【注意结构体Edge 需要重载<比较规则 - 用于sort】
枚举所有边  for j = 1 to m (edges简写 : e[M])
    if(边不在集合中)   【a -> b 边两点是否在同一个集合 : find(a) != find(b) 可回顾[连通块-并查集模板题](https://www.acwing.com/activity/content/code/content/4636671/)】
        边加入集合 p[find(a)] = find(b);
        
cnt统计加入集合的点边数 : 若为n - 1条则可构成最小生成树  

函数封装版

#include 
#include 
#include 

using namespace std;

const int N = 100010, M = 200010, INF = 0x3f3f3f3f;

int n, m;
int p[N];

struct Edge //【可以写c或w , (有邻接表不用w)会与w[]数组重名】
{
    int a, b, w;

    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[M]; //edges简写

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)
        {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF; //n个点选n - 1条边构造 - 最小生成树
    return res;
}

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

    int t = kruskal();

    if (t == INF) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

简化版

#include
#include
#include
using namespace std;

const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int n,m;

struct Edge    //【可以写c或w , w注意不要与邻接表w[idx]重名(此时不能用)】
{
    int a, b, c; //a->b : value = c 
    bool operator< (const Edge &t)const 
    {
        return c < t.c;
    }
}e[M]; //edges简写
int p[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 ++)
        scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].c);
    sort(e , e + m);

    for(int i = 1;i <= n;i++) p[i] = i;

    int res = 0

你可能感兴趣的:(合集,算法,c++,图论,数据结构,链表)