此笔记是我在考研复试复习时记录
1. 注意
memset(h, -1, sizeof h); //邻接表别忘了这句话
二分 如果l = mid 就要 mid = l + r + 1 >> 1;
二进制中几个1 while(x) x-=lowbit(x),res++;
vector去重 要先排序
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(),alls.end()), alls.end());
kmp中构建ne下标从2开始
匹配过程下标从1开始
构建和匹配中先while 后 if
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
初始化并查集
for (int i = 1; i <= n; i++) p[i] = i; // 初始化并查集
map.count(key) 返回数量
迪杰斯特拉 priority_queue<PII, vector<PII>, greater<PII>> heap; //小根堆
2. 快排
平均onlogn 最烂on2
#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
int n;
int a[N];
void quick_sort(int q[], int l, int r)
{
//递归的终止情况
if(l >= r) return;
//第一步:分成子问题
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
//第二步:递归处理子问题
quick_sort(q, l, j), quick_sort(q, j + 1, r);
//第三步:子问题合并.快排这一步不需要操作,但归并排序的核心在这一步骤
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++){
cin >> a[i];
}
quick_sort(a, 0, n-1);
for (int i = 0; i < n; i++){
cout << a[i] << " ";
}
}
#include <bits/stdc++.h>
using namespace std;
const int N=1000000+100; //容错
int a[N];
int main()
{
sort(a, a+n);
sort(a.begin(), a.end()); //vector
}
2.1. 快速选择算法
int quick_sort( int l, int r, int k){
//k 是想求的这个序列中 的第k个数
if (l == r){
return q[l];
}
int i = l-1;
int j = r+1;
int x = q[l];
while (i < j){
while(q[++i] < x);
while(q[--j] > x);
if( i < j)
swap (q[i], q[j]);
}
// 前面是快排
int sl = j - l + 1; //sl是j的次序
if (k <= sl) quick_sort(l, j, k); //如果k在左边的序列中,排左边
else quick_sort(j+1, r, k-sl);
}
3. 归并排序
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, tmp[r - l + 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(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}
3.1. 归并求逆序对,注意结果会超过int限制
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, tmp[r - l + 1];
while(i <= mid && j <= r)
if(q[i] <= q[j]) tmp[k++] = q[i++];
else{
tmp[k++] = q[j++];
res = mid - i + 1;
//从i 到mid 之间的所有数字都比 q[j]大,所以加mid-i+1
}
while(i <= mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}
4. 二分
//查找左边界 SearchLeft 简写SL
int SL(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
//查找右边界 SearchRight 简写SR
int SR(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1; //需要+1 防止死循环
if (check(mid)) l = mid; // 如果l = mid 就要加1
else r = mid - 1;
}
return r;
}
4.1. 浮点数二分
#include <iostream>
using namespace std;
double n;
double l = -10000;
double r = 10000;
int main(){
cin >> n;
while ( r - l > 1e-8){
double mid = (r + l)/ 2;
if (mid * mid * mid >= n){
r = mid;
}
else {
l = mid;
}
}
printf("%lf", l);
}
5. 前缀和
5.1. 前缀和
用来求 l ,r之间所有数的和 = s[r] - s[l-1]
#include <iostream>
using namespace std;
const int N = 100000 + 10;
int n,m;
int s[N];
int main(){
cin >> n >> m;
int k;
s[0] = 0;
for (int i = 1; i <= n; i ++){
cin >> k;
s[i] = s[i-1] + k; //求前缀和数组
}
int l, r;
while (m--){
cin >> l >> r;
cout << s[r] - s[l-1] << endl;
}
}
5.2. 差分
反向前缀和,用来让 l,r之间所有数据加上一个c
# include <iostream>
using namespace std;
const int N = 100000 + 10;
int n, m, b[N];
int main (){
cin >> n >> m;
int k;
int q = 0;
for (int i = 1; i <= n ; i++){
cin >> k;
b[i] = k - q;
q = k;
} //求差分数组, 差分数组前n个的和就是sn
int l, r , c;
while (m--){
cin >> l >> r >> c;
b[l] += c;
b[r+1] -= c;
}
//bl加上c, 后面的s都会加c,br-c和前面的+c抵消,所以+c限制在lr之间
int res = 0;
for(int i = 1; i <= n; i++){
res += b[i];
cout << res << " ";
}
}
5.3. 差分矩阵
- 求出差分矩阵
- 对差分矩阵中元素加c或者-c
- 求原矩阵
# include <iostream>
using namespace std;
const int N = 1010;
int n, m, q, s[N][N], d[N][N];
int main(){
cin >> n >> m >> q;
for (int i = 1; i <= n; i++){
for (int j = 1; j<= m; j++){
cin >> s[i][j];
d[i][j] = s[i][j] - s[i-1][j] - s[i][j-1] + s[i-1][j-1];
// 求差分矩阵
}
}
int x1, y1, x2, y2, c;
while (q--){
cin >> x1 >> y1 >> x2 >> y2 >> c;
d[x1][y1] += c;
d[x2+1][y1] -= c;
d[x1][y2+1] -= c;
d[x2+1][y2+1] += c;
// 公式
}
for( int i = 1; i <= n; i++){
for (int j = 1; j<=m; j++){
s[i][j] = d[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1]; //求原矩阵
cout << s[i][j] << " ";
}
cout << endl;
}
}
6. 双指针算法
6.1. 数组元素的目标和
#include <iostream>
using namespace std;
const int N = 100000 + 10;
int n, m , x;
int a[N];
int b[N];
int main (){
cin >> n >> m >> x;
for (int i = 0; i < n; i++){
cin >> a[i];
}
for (int i = 0; i < m; i++){
cin >> b[i];
}
int j = m-1;
for (int i = 0; i < n; i++){ //a数组从小到大,b数组从大到小
while (a[i] + b[j] > x) j--;
if(a[i]+b[j] == x) cout << i << " " << j;
}
}
6.2. 判断子序列
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);
int i = 0, j = 0;
while (i < n && j < m)
{
if (a[i] == b[j]) i ++ ;
j ++ ;
}
if (i == n) puts("Yes");
else puts("No");
return 0;
}
7. 二进制
7.1. 求二进制
int x = 5;
for (int i = 30; i >= 0; i--){
int u = x >> i & 1;
cout << u;
}
//输出 0000000000000000000000000000101
7.2. lowbit方法
返回最后一个1以及后面的0
int lowbit( int x ){
return x&(-x);
}
可以用来求一个二进制数里有几个1
int x;
cin>>x;
int res=0;
while(x) x-=lowbit(x),res++;
cout<<res<<' ';
8. 离散化
8.1. 求区间和
数轴范围10的9次方过于庞大,需要将坐标离散到123456
alls记录所有存入数和询问区间的下标,最大值可能有300000个
通过find方法(二分查找),找到某个数在alls中对应的下标,并返回
find也可以用哈希代替
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 300010;
typedef pair<int, int> PII;
int a[N], s[N];
int n, m;
vector<PII> add, query;
vector<int> alls;
//二分
int find (int x){
int l = 0;
int r = alls.size() - 1;
while(l < r){
int mid = (l + r) >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return l + 1;
}
int main (){
cin >> n >> m;
int x, c, l, r;
//存每个数下标到alls
for (int i = 0; i < n; i++){
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
//存每个区间值到alls
for (int i = 0; i < m; i++){
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
//alls排序去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(),alls.end()), alls.end());
//all中的下标离散化到a[N]并赋值
for (auto item : add){
int k = find(item.first);
a[k] += item.second;
}
//求前缀和
for (int i = 1; i <= alls.size(); i++){
s[i] = s[i-1] + a[i];
}
//求区间和
for (auto item: query){
int l = find(item.first);
int r = find(item.second);
int res = s[r] - s[l-1];
cout << res << endl;
}
}
9. 栈
9.1. 计算中缀表达式
题解 AcWing 3302. 表达式求值:多图讲解运算符优先级+详细代码注释 - AcWing
#include <iostream>
#include <algorithm>
#include <cstring>
#include <map>
#include <stack>
using namespace std;
stack<int> nums;
stack<int> ops;
unordered_map <char, int> opl = {{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
void eval(){ //计算函数,弹出两个数字和一个操作符,计算后压栈
auto b = nums.top(); nums.pop();
auto a = nums.top(); nums.pop();
auto c = ops.top(); ops.pop();
int x;
if (c == '+') x = a + b;
else if (c == '-') x = a - b;
else if (c == '*') x = a * b;
else x = a / b;
nums.push(x);
}
int main(){
string str;
cin >> str;
for (int i = 0; i < str.size(); i++){
auto c = str[i];
if (isdigit(c)){ //如果读到的是数字,读取数字并压栈
int x = 0;
while(isdigit(str[i])){
x = x * 10 + str[i ++] - '0';
}
i--;
nums.push(x);
}
// 读到左括号直接压栈
else if(c=='(') ops.push(c);
// 读到右括号,计算左括号和右括号之间的内容,右括号不压栈,算完之后左括号弹出栈
else if(c==')'){
while(ops.top() != '(') eval();
ops.pop();
}
else{ //如果ops内还有操作符并且栈顶不为左括号,比较两个操作的优先级
while(ops.size() && ops.top() != '(' && opl[ops.top()] >= opl[c]){
eval();
}
ops.push(c); //计算结束后将新操作符压入栈内
}
}
while (ops.size()) eval(); //如果有操作符未计算就循环计算
cout << nums.top() << endl;
return 0;
}
else读取其他操作符,如果读到的操作符优先级小于等于栈顶操作符,说明必须先计算栈内等级高的操作
如 1*3+4,读到+,必须先算1*3
同级也需要计算,如1+2+3
10. KMP算法
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
int n, m;
int ne[N];
char s[M], p[N];
int main()
{
cin >> n >> p + 1 >> m >> s + 1;
//构造next数组,next数组下标从1开始,ne[1] = 0
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
//匹配过程
for (int i = 1, j = 0; i <= m; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == n)
{
printf("%d ", i - n);
j = ne[j];
}
}
for (int i = 2, j = 0; i <= n; i ++){
while(j && p[i] != p[j+1]) j = ne[j];
if (p[i] == p[j+1]) j++;
ne[i] = j;
}
for (int i = 2, j = 0; i <= m; i++){
while (j && s[i] != p[j+1]) j = ne[j];
if (s[i] == p[j+1]) j++;
if (j == n){
printf("%d ", i - n);
j = ne[j];
}
}
return 0;
}
注意:
第一个while循环要判断 j, 因为j如果是0的话,说明当前在第一个和, 就没必要让 j = ne[j], 而且还会发生死循环
下标可能不至有一个,输出一个下标之后应该再让 j = ne[j]
11. 单调栈
使用:
情形一:寻找一个数左边第一个小于它的数
情形二:寻找一个数左边第一个小于它的数的下标
情形三:寻找一个数右边第一个大于它的数
情形四:寻找一个数右边第一个大于它的数的下标
#include <iostream>
#include <stack>
using namespace std;
const int N = 100010;
stack<int> s;
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
while ( s.size() && s.top() >= x) s.pop() ;
if (s.size() == 0) cout << "-1 ";
else cout << s.top() << " ";
s.push(x);
}
return 0;
}
12. 单调队列
12.1. 滑动窗口求最值
求最小值时,维护一个队列,队列里存放原数组数的下标,队头所代表的一定是最小值
队伍移动时,左边是队头hh,右边是队尾tt
在队伍向前移动时,循环判断,如果新的数小于队尾的数,则删除队尾数(因为此数一定不可能是当前窗口的最小值),直到新的数大于队尾的数,或者整个队伍为空,这样能使整个队列呈现一种递增的趋势,从而保证队头一定是最小的
#include <iostream>
using namespace std;
const int N = 1000000 + 10;
int n, k;
int a[N], q[N];
int main (){
cin >> n >> k;
for (int i = 1; i <= n; i ++){
cin >> a[i];
}
//求最小值
int hh = 0;
int tt = -1;
for (int i = 1; i <= n; i++){
while (hh <= tt && q[hh] < i - k + 1) hh ++; //q[hh]代表队头下标,判断是否在窗口内
while (hh <= tt && a[q[tt]] >= a[i]) tt--; // 如果新扫描的数比队尾小,删队尾
q[++ tt] = i;
if (i >= k) cout << a[q[hh]] << " ";
}
cout << endl;
//求最大值
hh = 0;
tt = -1;
for (int i = 1; i <= n; i++){
while (hh <= tt && q[hh] < i - k + 1) hh ++;
while (hh <= tt && a[q[tt]] < a[i]) tt--;
q[++ tt] = i;
if (i >= k) cout << a[q[hh]] << " ";
}
}
13. Trie树(字典树)
高效的存储和查找字符串集合的数据结构
统计词频
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100000 + 10;
int n;
int son[N][26];
int idx;
int cnt[N];
char op, s[N];
void insert (char str[] ){
int p = 0;
for (int i = 0; str[i]; i ++){
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p] ++;
}
int query(char str[] ){
int p = 0;
for (int i = 0; str[i]; i++){
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
int main (){
cin >> n;
while (n--){
cin >> op >> s;
if(op == 'I') insert(s);
else{
cout << query(s) << endl;
}
}
}
13.1. 最大异或对
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100000 + 10;
const int M = 3000000;
int idx;
int son[M][2];
int n;
int a[N];
int res =0;
void insert (int x){
int p = 0;
for (int i = 30; i >= 0; i--){
int u = x >> i & 1;
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
}
int query (int x){
int p = 0;
int k = 0;
for (int i = 30; i >= 0; i--){
int u = x >> i & 1;
if (son[p][!u]){
p = son[p][!u];
k += 1 << i;
}
else p = son[p][u];
}
return k;
}
int main (){
cin >> n;
for (int i = 0; i < n; i++){
cin >> a[i];
insert(a[i]);
}
for (int i = 0; i < n; i++){
int k = query(a[i]);
res = max(k, res);
}
cout << res;
}
14. 并查集
14.1. 合并与查找
#include <iostream>
using namespace std;
const int N = 100000 + 10;
int n, m;
char op[2];
int a, b;
int p[N]; // p数组存了此节点的父亲节点 p[x] = x 代表此节点是根节点, 此时x就是集合的编号
//find函数 查找祖宗节点 压缩路径
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; // 初始化并查集
while (m--){
cin >> op >> a >> b;
if (op[0] == 'M'){
p[find(a)] = find(b);
}
else{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
}
14.2. 统计集合内元素个数
用一个size数组统计
#include <iostream>
using namespace std;
const int N = 100000 + 10;
int n, m;
char op[2];
int a, b;
int p[N]; // p数组存了此节点的父亲节点 p[x] = x 代表此节点是根节点, 此时x就是集合的编号
int s[N]; //size数组存储这个集合的元素,只有根节点的size有意义
//find函数 查找祖宗节点 压缩路径
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++) {
// 初始化,让每个节点指向自己,size = 1
p[i] = i;
s[i] = 1;
}
while (m--){
cin >> op ;
if (op[0] == 'C'){
cin >> a >> b;
int pa = find(a);
int pb = find(b);
if (pa != pb){ // 如果两个点不在一个集合中,合并然后操作size
p[pa] = pb;
s[pb] += s[pa];
}
}
else if (op[1] == '1'){
cin >> a >> b;
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
else if (op[1] == '2'){
cin >> a;
cout << s[find(a)] << endl;
}
}
}
15. 堆排序
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], cnt;
// down操作
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];
cnt --; //让堆顶等于堆尾,同时删掉队尾,再down堆顶
down(1);
}
puts("");
return 0;
}
16. DFS
16.1. 回溯输出全排列
#include <iostream>
using namespace std;
const int N = 9;
int n;
bool st[N];
int nums[N];
void dfs (int u){
if (u == n) {
for (int i = 0; i < n; i ++ ) cout << nums[i] << " ";
puts("");
}
for (int i = 1; i <= n; i ++){
if (!st[i]){
nums[u] = i;
st[i] = true;
dfs (u + 1);
st[i] = false;
}
}
}
int main(){
cin >> n;
dfs(0);
}
16.2. n皇后
#include <iostream>
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool col[N], dg[N * 2], udg[N * 2];
void dfs(int u)
{
if (u == n)
{
for (int i = 0; i < n; i ++ ) puts(g[i]);
puts("");
return;
}
for (int i = 0; i < n; i ++ )
if (!col[i] && !dg[u + i] && !udg[n - u + i])
{
g[u][i] = 'Q';
col[i] = dg[u + i] = udg[n - u + i] = true;
dfs(u + 1);
col[i] = dg[u + i] = udg[n - u + i] = false;
g[u][i] = '.';
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
g[i][j] = '.';
dfs(0);
return 0;
}
17. BFS
bfs可以用于搜索权重为1的最短距离
17.1. 迷宫问题最短路径
走迷宫,遇到1不能走
#include <iostream>
#include <queue>
#include <algorithm>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 100 + 10;
int a[N][N];
int d[N][N];
queue<PII> q;
int n, m;
int bfs(){
memset(d, -1, sizeof d);
d[0][0] = 0;
q.push({0, 0});
int dx[4] = {0, -1, 0, 1};
int dy[4] = {-1, 0, 1, 0};
while (q.size()){
auto t = q.front();
q.pop();
for (int i = 0; i < 4; i++ ){
int x = t.first + dx[i];
int y = t.second + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && a[x][y] == 0 && d[x][y] == -1){
// 需要判断边界,判断是否可走,判断是否走过
d[x][y] = d[t.first][t.second] + 1;
q.push({x, y});
}
}
}
return d[n-1][m-1]; //返回的值一定是第一次走过的距离,所以距离一定最短
}
int main(){
cin >> n >> m;
for (int i = 0; i < n; i++){
for (int j = 0; j < m ; j++){
cin >> a[i][j];
}
}
cout << bfs();
}
17.2. 八数码问题
还是bfs,关键在于如何存储每一个状态
一个图压缩成一个string存储,用map存储初试状态到结尾状态的距离,一次变换距离加一
由于bfs的性质,第一次出现end时就是最短的变换次数
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
queue<string> q;
unordered_map<string, int> d;
int bfs(string start){
d[start] = 0;
q.push(start);
string end = "12345678x";
int dx[4] = {1,0,-1,0};
int dy[4] = {0,1,0,-1};
while(q.size()){
string s = q.front();
int distance = d[s];
q.pop();
if (s == end){
return d[s];
}
int k = s.find('x');
int a = k/3;
int b = k%3;
for (int i = 0; i < 4; i ++){
int x = a + dx[i];
int y = b + dy[i];
if (x>=0 && x<3 && y >= 0 && y < 3){
string t = s;
swap(t[k], t[x*3+y]);
if (!d.count(t)){
d[t] = distance + 1;
q.push(t);
}
}
}
}
return -1;
}
int main(){
string start;
for (int i = 0; i < 9; i++){
char c;
cin >> c;
start +=c;
}
cout << bfs(start);
}
18. 二叉树的存储与遍历
18.1. 已知前序遍历输出
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
string s;
int k;
//abc##de#g##f###
void dfs(){
if (s[k] == '#') {
k++;
return;
}
char r = s[k++];
dfs(); //左树
cout << r << " "; //输出
dfs(); //右树
}
int main (){
cin >> s; // c b e g d f a
dfs();
}
后序的输出就是改一下位置
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
string s;
int k;
//abc##de#g##f###
void dfs(){
if (s[k] == '#') {
k++;
return;
}
char r = s[k++];
dfs(); //左树
dfs(); //右树
cout << r << " "; //输出
}
int main (){
cin >> s; // c b e g d f a
dfs();
}
18.2. 已知前序后序输出中序
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
string pre, in;
int k;
// 给出一段前序和中序,dfs
void dfs(string pre, string in){
if (pre.empty()) return;
char root = pre[0];
int k = in.find(root);
dfs(pre.substr(1, k), in.substr(0, k)); //遍历左树
dfs(pre.substr(k + 1), in.substr(k + 1)); //遍历右树
cout << root; //后序输出
}
int main (){
while(cin >> pre >> in){
dfs(pre, in);
cout << endl;
}
}
18.3. 已知后序中序输出前序
void dfs(string ba, string in){
if (ba.empty()) return;
char root = ba[ba.size() - 1];
int k = in.find(root);
idx ++;
if (idx == n) cout << root;
dfs(ba.substr(0, k), in.substr(0, k)); //遍历左树
dfs(ba.substr(k, ba.size() - k - 1), in.substr(k + 1)); //遍历右树
}
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
const int N = 50000 + 10;
using namespace std;
int k;
int n;
int a[N], b[N];
unordered_map <int, int> p; //用于记录某个节点在中序遍历中的下标位置
int ans;
void dfs(int al, int ar, int bl, int br){
if (al > ar) return; //说明串为空
int root = a[ar];
int k = p[root];
cout << root; //输出前序
dfs(al, k-1 - bl + al, bl , k-1);
dfs(k - bl + al, ar-1, k+1, br);
}
int main (){
cin >> n;
int c;
for (int i = 0; i < n; i++){
cin >> a[i];
}
for (int i = 0; i < n; i++){
cin >> b[i];
p[b[i]] = i;
}
dfs(0,n-1,0,n-1);
cout << ans;
}
18.4. 最近父节点算法
计算二叉树中两个节点之间的距离,找到他们的公共父节点,并记录每个节点到根的距离,就可以快速得出
int c = find_lca(a, b);
int res = dist[a] + dist[b] - 2*dist[c];
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1000 + 10;
int t, n, m;
int l[N], r[N], p[N];
int dist[N];
void dfs(int u, int d){
dist[u] = d;
if (l[u] != -1) dfs(l[u], d+1);
if (r[u] != -1) dfs(r[u], d+1);
}
void bfs(int start){
//memset(dist, 0, sizeof dist);
dist[start] = 0;
queue<int> q;
q.push(start);
while(q.size()){
int t = q.front();
q.pop();
if (l[t] != -1) q.push(l[t]), dist[l[t]] = dist[t] + 1;
if (r[t] != -1) q.push(r[t]), dist[r[t]] = dist[t] + 1;
}
}
int find_lca(int a, int b){
if (dist[a] < dist[b]) return find_perant(b, a);
while (dist[a] > dist[b]) a = p[a];
while (a != b) a = p[a], b = p[b];
return a;
}
int main(){
cin >> t;
while(t--){
cin >> n >> m;
for (int i = 1; i <= n; i++){
int a, b;
cin >> a >> b;
l[i] = a;
r[i] = b;
if (a != -1) p[a] = i;
if (b != -1) p[b] = i;
}
bfs(1);
while(m--){
int a, b;
cin >> a >> b;
int c = find_lca(a, b);
int res = dist[a] + dist[b] - 2*dist[c];
cout << res << endl;
}
}
}
19. 图的存储与搜索
19.1. 邻接表存储法
图一般用临接表
无向图是特殊的有向图,建立两条边即可
DFS时间复杂度O(n+m)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = N*2; //最多N个节点,最多M条边,M为N的2倍
int n;//需要输入n-1个边
int h[N], e[M], ne[M], idx; //e存某个idx的值,ne存下一个点的idx,h存链表的头的idx,总共N个
bool st[N]; //看看这个点是否走过
void add(int a,int b){ //建立单向边,从a至b
e[idx] = b; // 编号为idx的点值为b
ne[idx] = h[a]; //b指向a的头
h[a] = idx;
idx ++;
}
void dfs(int u){
st[u] = true;//说明这个点已经走过去了
for(int i = h[u]; i != -1; i = ne[i]){ //遍历u的所有子节点
int j = e[i]; //j代表u的联通节点,u->j
if( !st[j] ){ //这个点已经遍历过,那么看u的下一个子节点
dfs(j); //递归j
}
}
}
int main(){
cin >> n;
memset(h, -1, sizeof(h));//将每个节点的链表设置为空
for(int i = 1; i < n; i++){//这里不能用while(n--)的形式,因为要用到n作为树的总节点数
int a,b;
cin >> a >> b;
add(a, b);
add(b, a); // 无相图要建立两条边
}
dfs(1);
return 0;
}
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010, M = N*2; //最多N个节点,最多M条边,M为N的2倍
int n;//需要输入n-1个边
int h[N], e[M], ne[M], idx; //e存某个idx的值,ne存下一个点的idx,h存链表的头的idx,总共N个
bool st[N]; //看看这个点是否走过
queue<int> q
void add(int a,int b){ //建立单向边,从a至b
e[idx] = b; // 编号为idx的点值为b
ne[idx] = h[a]; //b指向a的头
h[a] = idx;
idx ++;
}
void bfs(int u){
q.push(u);
while(q.size()){
int a = q.front();
q.pop();
for (int i = h[a]; i != -1; i = ne[i]){
int k = e[i];
if (!st[k]){
q.push(k);
st[k] = true;
}
}
}
int main(){
cin >> n;
memset(h, -1, sizeof(h));//将每个节点的链表设置为空
for(int i = 1; i < n; i++){//这里不能用while(n--)的形式,因为要用到n作为树的总节点数
int a,b;
cin >> a >> b;
add(a, b);
add(b, a); // 无相图要建立两条边
}
bfs(1);
return 0;
}
19.2. DFS搜索树的重心
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100000 + 10;
const int M = N*2;
int h[N], e[M], ne[M], idx;
int n;
bool st[N];
int ans = N;
void add(int a, int b){ // a指向b,只需要创建b
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
int dfs(int u){
st[u] = true;
int sum = 1; //记录此节点为根的子树总共有几个点
int res = 0; //记录删除此节点,剩余其他树中的节点个数最大值
for (int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if (!st[j]){
//cout << j << " " ;
int s = dfs(j);
sum += s;
res = max(res, s); // 循环结束后算出了这个点指向的几个子树中的最大值
}
}
res = max(res, n - sum); // 还要和这个节点上方的树进行比较
ans = min(ans, res); //还要算出每删除一个点得到的所有结果中的最小值
return sum;
}
int main (){
cin >> n;
memset(h, -1, sizeof h);
int a, b;
int k = n-1;
while(k--){
cin >> a;
cin >> b;
add(a, b);
add(b, a);
}
dfs(1);
cout << ans;
}
19.3. BFS查找到n点最短的距离
int bfs (int start){
q.push(start);
memset(d, 0, sizeof d); //d存储所有点到start的距离,初始为0
while(q.size()){
int a = q.front(); //弹出队头
if (a == n){
return d[a]; //如果队头是n,直接返回d[n], 当n出现在队列时说明已经找到最短路
}
q.pop();
for (int i = h[a]; i != -1; i = ne[i]){
int k = e[i];
if (!st[k]){
q.push(k);
st[k] = true; //用st记录找过的点
d[k] = d[a] + 1;
// if (k == n){
// return d[k];
// }
//不能在这里判断是不是n,因为有可能只有一个点,不会运行到这里
}
}
}
return -1;
}
当n出现在队列时说明已经找到最短路
还有一种思路,在运行结束直接返回d[n]
int bfs()
{
memset(d, -1, sizeof d);
queue<int> q;
d[1] = 0;
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (d[j] == -1) //d[j] == -1说明没找过这个点
{
d[j] = d[t] + 1;
q.push(j);
}
}
}
return d[n];
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/47104/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
20. 拓扑排序
#include<bits/stdc++.h>//万能头文件
using namespace std;
const int N=1e5+10;//好习惯
int q[N],hh=0,tt=-1;//hh是head的缩写,tt->tail,hh指向的是头元素,可以直接取,tt指向的是末尾元素,同样的可以直接取得
int h[N],e[N],ne[N],idx;//此处为构建双重链表
int n,m;//题目要求
int in[N];//这代表每一个点的入度,看起来清晰一些
void add(int a,int b){//让a指向b
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
bool topsort(){//这好像bfs啊
for(int i=1;i<=n;i++){//寻找入度为0的点,并且将其装入queue
//此处注意,遍历需要从1开始到n结束,切不可写成for(int i=0;i<n;i++),痛苦的教训
if(in[i]==0){
q[++tt]=i;
}
}
while(hh<=tt){//当queue不为空的话就继续
int t=q[hh++];//取得队列头的元素,其入度为0
for(int i=h[t];i!=-1;i=ne[i]){//遍历这个点所有的子节点,将子节点的入度减一,同时判断子节点的入度是否为0,如果为0那么就可以进入队列了
int j=e[i];//i是idx地址,j是节点的代表,需要通过e数组转化
in[j]--;//j节点的入度减一
if(in[j]==0){//j节点如果入度为0的话那么就可以加入queue了(此语句可于上面的语句合并)
q[++tt]=j;
}
}
}
return tt==n-1;//要记得加
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof(h));//记得这个初始化,否则会TLE,痛苦的教训
for(int i=0;i<m;i++){//完成表的初始化
int a,b;
cin>>a>>b;
add(a,b);
in[b]++;//有a->b,那么b的入度肯定要加一
}
if(!topsort()){
cout<<-1<<endl;
}else{
for(int i=0;i<n;i++){//逐个输出queue中的元素
cout<<q[i]<<' ';
}
}
return 0;
}
21. Dijkstra求最短路
21.1. 朴素
- 首先,定义了常量 N 为 510,表示最大的顶点数。
- 接着定义了 n 和 m,分别表示图中的顶点数和边数。
- 使用二维数组 g[N][N] 来表示图的邻接矩阵,存储了顶点之间的边权重信息。
- 定义了数组 dist[N] 用于存储从起始点到各个顶点的最短距离。
- 定义了数组 st[N] 用于标记顶点是否已经确定了最短路径。
在 dijkstra 函数中:
- 首先将 dist 数组初始化为一个很大的值(这里使用 0x3f3f3f3f 表示),并且将起始点的距离设为 0。
- 然后进行 n-1 次循环,每次选择一个未确定最短路径的顶点 t,并更新从起始点出发到达其他顶点的最短距离。
- 在每次循环中,找出当前距离起始点最近的顶点 t,并将其标记为已确定最短路径。然后通过 t 更新与 t 相邻的顶点的最短距离。
- 最后判断终点的最短距离,如果为初始值,则输出 -1,否则输出最短距禎。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510;
int n,m;
int g[N][N]; //邻接矩阵存储
int dist[N];
bool st[N];
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i++){ //n-1次循环
int t = -1;
for (int j = 1; j <= n; j++){
if (!st[j] && (t == -1 || dist[t] > dist[j])){
t = j;
}
}
//cout << t << endl;
st[t] = true;
for (int j = 1; j<= n; j++){
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main (){
cin >> n >> m;
memset(g, 0x3f, sizeof g);
int a, b, c;
for (int i = 0; i < m; i++){
cin >> a >> b >> c;
if (a != b){
g[a][b] = min(g[a][b], c);
}
}
int res = dijkstra();
cout << res;
}
21.2. 堆优化
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 150000 + 10;
int n,m;
int h[N], e[N], ne[N], w[N];
int dist[N];
bool st[N];
int idx;
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; //小根堆
heap.push({0, 1}); //{距离,节点}
while(heap.size()){
auto t = heap.top();
heap.pop();
int p = t.second;
int distance = t.first;
if (st[p]) continue;
st[p] = true;
for (int i = h[p]; i != -1; i = ne[i]){
int j = e[i];
if (dist[j] > distance + w[i]){
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main (){
cin >> n >> m;
memset(h, -1, sizeof h);
int a, b, c;
for (int i = 0; i < m; i++){
cin >> a >> b >> c;
if (a != b){
add(a, b, c);
}
}
int res = dijkstra();
cout << res;
}
22. Floyd求最短路
三次循环
k个阶段
只经过1-k的点,求 i 到 j 的最短路径
可以有负边,但不能有负边回路
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];
void floyd()
{
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main()
{
scanf("%d%d%d", &n, &m, &Q);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c);
}
floyd();
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;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/48531/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
23. 最小生成树
最小生成树(Minimum Spanning Tree,MST)是指在一个连通加权无向图中找到一棵包含图中所有顶点的树,并且使得树的边的权值之和最小。换句话说,最小生成树是原图的一棵包含了所有顶点并且边的权重之和最小的子树。
常用的算法来找到最小生成树的包括:
- 普里姆算法(Prim's Algorithm):
-
- 普里姆算法从一个初始顶点开始,逐步将与当前树相邻的最短边所连接的顶点加入树中,直到树包含了图中的所有顶点为止。
- 克鲁斯卡尔算法(Kruskal's Algorithm):
-
- 克鲁斯卡尔算法则是通过不断选择权重最小的边,并且保证这条边不会形成环,来逐步构建最小生成树。
最小生成树在实际中有着广泛的应用,比如在通信网络设计、电力输送线路规划等领域。通过找到一个图的最小生成树,可以以最优的方式建立一个连接所有节点的网络结构,并且保证总成本最小。
23.1. 朴素Prim
- 与dijkstra不同,prim需要迭代n次
- 最小生成树是针对无向图的,所以在读入边的时候,需要赋值两次
- 要先累加再更新,避免t有自环,影响答案的正确性。后更新不会影响后面的结果么?不会的,因为dist[i]为i到集合S的距离,当t放入集合后,其dist[t]就已经没有意义了,再更新也不会影响答案的正确性。
- 需要特判一下第一次迭代,在我们没有做特殊处理时,第一次迭代中所有点到集合S的距离必然为无穷大,而且不会进行更新(也没有必要),所以不需要将这条边(第一次迭代时,找到的距离集合S最短的边)累加到答案中,也不能认定为图不连通。
- 如果需要设置起点为i的话,在初始化dist数组之后,dist[i] = 0即可,这样也可以省去每轮迭代中的两个if判断。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
int g[N][N];
int d[N];
bool st[N];
int prim()
{
memset(d,0x3f,sizeof d);
d[1] = 0; //d[1] = 0这样就不用判断i是不是等于0
int res = 0;//权重之和
for(int i = 0;i<n;i++) //循环n次
{
//寻找不属于集合且距离集合最小的点
int t = -1;
for(int j = 1;j<=n;j++){
if(!st[j] && (t == -1 || d[t] > d[j]))
t = j;
}
if(d[t] == INF) return INF;
//把点t加到集合当中去,更新权值
res += d[t];
st[t] = true;
//更新在后面是因为可能有自环,提前更新会改变dist[t]
for(int j = 1;j<=n;j++) d[j] = min(d[j],g[t][j]);
}
return res;
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
int t = prim();
if (t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
23.2. 堆优化prim
23.3. Kruskal算法
思路:
将所有的边按权值从小到大排序(用自定义结构体实现)
然后从小到大枚举每一条边,如果这条边的两个点没有连通则取这条边
如果之前就连通了则不取(这里判断是否连通和连通两个点可以用并查集来实现)
最后记录一下取了多少条边cnt,如果cnt小于n-1,说明这个树不是连通的,如果cnt大于n-1,则输出res所有边之和。
#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{
int a, b, w;
}edges[M];
bool cmp(Edge e1, Edge e2){
return e1.w < e2.w;
}
int find(int x){
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal(){
sort(edges, edges + m, cmp);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ ) //从小到大取每一一条边
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) //如果ab不连通,就让ab联通
{
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
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;
}