文章目录
- 基础算法
- 数据结构
- 搜索与图论
- 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 1∼109 范围内),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5
#include<iostream>
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
j−l+1,若
j
−
l
+
1
>
=
k
j - l + 1 >= k
j−l+1>=k ,则第k大的数在前一段,
否则在后一段中的第
k
−
(
j
−
l
+
1
)
k - (j - l + 1)
k−(j−l+1)的位置
#include <iostream>
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 <bits/stdc++.h>
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 <iostream>
#include <algorithm>
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 1∼109 范围内),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5
二路归并
#include <iostream>
#include <cstring>
#include <algorithm>
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 个元素,如果满足 i<j 且 a[i]>a[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<iostream>
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 <iostream>
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 <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
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 <iostream>
#include <algorithm>
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;
}
补充
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<type>() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。
通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num,greater<type>() ):从数组的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=mid−1
我们最终找的边界是一个性质在右半区不符合,在左半区符合的性质。并找到符合性质的左半区的右边界点。
寻找右分界点(左半区的右边界)
整数二分算法模板 —— 模板题 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<iostream>
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<bits/stdc++.h>
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 <iostream>
#include <vector>
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 <iostream>
#include <vector>
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<iostream>
#include<vector>
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 <iostream>
#include <vector>
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 <iostream>
#include <vector>
#include <algorithm>//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 <iostream>
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 <iostream>
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=Sx−1,y+Sx,y−1−Sx−1,y−1+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,y2−Sx1−1,y2−Sx2,y1−1+Sx−1,y−1
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
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 <iostream>
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<iostream>
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 <iostream>
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得到差分数组
二维差分好题解-图片来源
#include <iostream>
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 <iostream>
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 <iostream>
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
利用单调性-双指针
#include <iostream>
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<iostream>
#include<cstring>//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 <iostream>
#include <cstring>
#include <algorithm>
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 <iostream>
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<iostream>
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<iostream>
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<iostream>
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 <iostream>
#include <vector>
#include <algorithm>
#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 <iostream>
#include <vector>
#include <algorithm>
#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 <bits/stdc++.h>
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 <iostream>
#include <algorithm>
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<iostream>
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<iostream>
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 个插入的结点
(相似模板化-有助于记忆)
#include <iostream>
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 <iostream>
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<iostream>
#include<stack>
#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
231−1。
题目中的整除是指向 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<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
优先级高的先计算完: 若当前运算符优先级 <= 之前已经入栈的运算符, 则需先计算(调用eval)先前入栈的运算符再入栈当前操作符, 反之直接入栈
循环完若操作栈非空,则不断eval()直到没有操作符, 最后运算结果为num.top()
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>//三个参数key,value,类(一般省略)
#include <stack>
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 <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map> //三个参数key,value,类(一般省略)
#include <stack>
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 <iostream>
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 <iostream>
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 <iostream>
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总和佬们]
先用普通队列 :先暴力, 再看能否删去非答案元素, 再想是否用单调性-二分查找优化
模拟队列 hh = 0, tt = -1, 出队hh++, 队列不空 hh <= tt
【看做双端队列deque :允许队尾出队 tt–】
队头 <— (从队尾不断入队) [不超过边界] : 整个模拟队列判断过程下标不断向右增大移动
简记:①判断队头是否已经滑出窗口(在边界内hh++) ②删除所有逆序对(tt--) ③最小的入队(++tt) **
④i到达初始窗口末尾(窗口大小k,下标k-1)开始输出,(维护严格单调**递增队列-队头最小) : 下标取值a[q[hh]]
#include <iostream>
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<iostream>
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;
}
#include <bits/stdc++.h>
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 <iostream>
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
2∗104
输入样例:
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 <iostream>
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 <iostream>
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;
}
理解图
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 <iostream>
#include <algorithm>
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;
}
暴力枚举-(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 <iostream>
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 <iostream>
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 <iostream>
#include <cstring>
#include <algorithm>
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 <iostream>
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的根结点将会重叠,导致输出错误!
#include<bits/stdc++.h>
#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:与根节点同类
一代吃一代循环【题目仅分三类】
#include <iostream>
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 <iostream>
#include <cstring>
#include <algorithm>
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<iostream>
#include<cstdio> //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 <iostream>
#include <algorithm>
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<key类型, value类型> heap;
(考研模拟, 竞赛用STL哈希表)
#include <iostream>
#include <cstring>
#include <algorithm>
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 <iostream>
#include <cstring>
#include <algorithm>
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 <iostream>
#include <algorithm>
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是大写字母,只是这样方便观察.
*/
搜索与图论
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<iostream>
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<iostream>
#include<algorithm>
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<iostream>
#include<algorithm>
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 <iostream>
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 <iostream>
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;
}
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 <iostream>
#include <cstring>
#include <algorithm>
#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<iostream>
#include<cstring>
#include <algorithm>
#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 <iostream>
#include <cstring>
#include <queue>
#include<stack>
#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<PII> path : FIFO先进先出 -正序输出 【若逆序存入,则用stack<PII> path : LIFO后进先出 -正序输出】
{
x = path.front().x, y = path.front().y;
path.pop();
printf("%d %d\n", x, y); //逆序输出路径 【正序改用栈stack<PII> 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<string, int> d
, BFS存储状态 queue<string> 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 <iostream>
#include <algorithm>
#include <unordered_map>
#include <queue>
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
1≤n≤105
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
dfs邻接表深搜
邻接表存有向边, 若是无向图则边数 = 有向边 * 2 (最多边数 = 最多点数 * 2)【注意开数组大小】
疑点:为什么不用搜n遍, 重心是怎么确定的??
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
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<cstdio>
#include<cstring>
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<cstdio>
#include<cstring>
#include<queue>
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 <iostream>
#include <cstring>
#include <algorithm>
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 <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
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 <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
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<dist[点编号], 点编号>小根堆 + 邻接表)
【正权边堆优化版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);
由于串联,如图:
若限制但从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 <cstring>
#include <iostream>
#include <algorithm>
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 <cstring>
#include <iostream>
#include <algorithm>
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 <cstring>
#include <iostream>
#include <algorithm>
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 <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
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队列<queue>库函数-BFS使用: q.front(), q.pop(), q.push(), q.size()或q.empty()
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N]; //在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 <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
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]);
#include <iostream>
#include <cstring>
#include <algorithm>
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<iostream>
#include<algorithm>
#include<cstring>
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 <cstring>
#include <iostream>
#include <algorithm>
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<iostream>
#include<algorithm>
#include<cstring>
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,cnt = 0; //统计n个点之间的最小边权和(最小生成树) [若不是点都联通,则不可能]
for(int i = 0;i < m;i++) //枚举所有边
{
int a = e[i].a ,b = e[i].b , c = e[i].c;
a = find(a), b = find(b);
if(find(a) != b) //边不在集合中就加入 【操作回顾:连通块中点的数量 - 并查集模板题】
{
res += c;
cnt ++;
p[a] = b;
}
}
if(cnt < n - 1) puts("impossible"); //没有选到n - 1条边【无法构造最小生成树】
else printf("%d\n",res);
return 0;
}
土法赋值hh
for(int i = 0;i < m;i ++)
{
int &a = edges[i].a, &b = edges[i].b, &w = edges[i].w;
scanf("%d%d%d", &a, &b, &w);
}
AcWing 861. 二分图的最大匹配
给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
输入格式
第一行包含三个整数 n1、 n2 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围
1≤n1,n2≤500,
1≤u≤n1,
1≤v≤n2,
1≤m≤
1
0
5
10^5
105
输入样例:
2 2 4
1 1
1 2
2 1
2 2
输出样例:
2
匈牙利算法
在这里插入代码片
数学知识-动态规划-贪心【合集2】
时空复杂度分析
一般ACM或者笔试题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在
1
0
7
∼
1
0
8
10^7∼10^8
107∼108 为最佳。
下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:
1. n ≤ 30 n≤30 n≤30, 指数级别, dfs+剪枝,状态压缩dp
2. n ≤ 100 n≤100 n≤100 => O ( n 3 ) O(n^3) O(n3),floyd,dp,高斯消元
3. n ≤ 1000 n≤1000 n≤1000 => O ( n 2 ) , O ( n 2 l o g n ) O(n2),O(n^2logn) O(n2),O(n2logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
4. n ≤ 10000 n≤10000 n≤10000 => O ( n ∗ n ) O(n∗\sqrt n) O(n∗n),块状链表、分块、莫队
5. n ≤ 100000 n≤100000 n≤100000 => O ( n l o g n ) O(nlogn) O(nlogn) => 各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、Kruskal、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树
6. n ≤ 1000000 n≤1000000 n≤1000000 => O ( n ) O(n) O(n), 以及常数较小的 O ( n l o g n ) O(nlogn) O(nlogn)算法 => 单调队列、 hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O ( n l o g n ) O(nlogn) O(nlogn) 的做法:sort、树状数组、heap、dijkstra、spfa
7. n ≤ 1 0 7 n≤10^7 n≤107 => O ( n ) O(n) O(n),双指针扫描、kmp、AC自动机、线性筛素数
8. n ≤ 1 0 9 = > O ( n ) n≤10^9 => O(\sqrt n) n≤109=>O(n),判断质数
9. n ≤ 1 0 18 n≤10^{18} n≤1018 => O ( l o g n ) O(logn) O(logn),最大公约数,快速幂,数位DP
10. n ≤ 101000 n≤101000 n≤101000 => O ( ( l o g n ) 2 ) O((logn)^2) O((logn)2),高精度加减乘除
11. n ≤ 10100000 n≤10100000 n≤10100000 => O ( l o g k × l o g l o g k ) O(logk×loglogk) O(logk×loglogk),k表示位数,高精度加减、FFT/NTT