基础算法模板
由数据范围反推算法复杂度以及算法内容#
, 指数级别, +剪枝,状态压缩 => , , ,高斯消元 => , , ,二分,朴素版 、朴素版 、 => ,块状链表、分块、莫队 => => 各种 ,线段树、树状数组、 、 、拓扑排序、 + 、 + 、 、 、求凸包、求半平面交、二分、 分治、整体二分、后缀数组、树链剖分、动态树 => , 以及常数较小的 算法 => 单调队列、 、双指针扫描、并查集, 、AC自动机,常数比较小的 的做法: 、树状数组、 、 、 => ,双指针扫描、 、 自动机、线性筛素数 => ,判断质数 => ,最大公约数,快速幂,数位DP => ,高精度加减乘除 => , 表示位数,高精度加减、 /
1.排序#
1.快速排序#
-
算法内容:
- 确定分界点
(一般为 ) - 调整区间,将
区间划分为左右两段,利用双指针使得左边的数都小于等于 ,右边的数都大于等与 - 递归处理左右两边
- 确定分界点
-
时间复杂度
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int q[N];
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
//我们要实现的是两个指针每次比较完之后,如果符合条件就向中间移动一格。
//可以用do while实现:先向中间移动一格再去比较,这一次的移动实际上是上一次的结果,
//所以最后开始两个指针要指在边界外 l - 1 和 r + 1,执行完do语句之后就会变为l和r
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()
{
int n;
cin >> n;
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(q, 0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
延申:快速选择求第k小的数
-
算法内容:
每一次调整区间之后只递归处理第k个数所在的区间
-
时间复杂度:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int q[N], n, k;
int quick_select(int q[], int l, int r, int k)
{
if (l >= r) return q[l];
int x = q[(l + r) >> 1], i = l - 1, j = 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]);
}
int sl = j - l + 1; //[l, j]区间上由多少个数
if (k <= sl) return quick_select(q, l, j, k);
else return quick_select(q, j + 1, r, k - sl);
}
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
cout << quick_select(q, 0, n - 1, k);
return 0;
}
2.归并排序#
- 算法内容:
- 确定分界点
- 以
将 区间分为左右两段,递归排序左边和右边,得到左边和右边两段有序序列 - 用双指针将两段序列合并为一个序列
- 确定分界点
- 时间复杂度:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N], tmp[N];
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);
//归并[l, mid]和[mid + 1, r]两段有序序列
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
{
//把小的元素放到tmp数组
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 (i = l, j = 0; i <= r; i ++, j ++ )
q[i] = tmp[j];
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
merge_sort(a, 0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", a[i]);
return 0;
}
延申:求逆序对的数量
-
算法内容:
-
以
为分界点递归排序左右两个区间 -
将逆序对
分为三类: 都在左边区间,数量为merge_sort(q, l, mid)
在左边, 在右边,数量为merge_sort(q, mid + 1, r)
都在右边
-
先令
res = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);
。然后在归并的过程中,每一次 开始大于 的时候res += mid - i + 1
如图:
-
-
时间复杂度:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n;
int q[N], tmp[N];
LL res;
LL merge_sort(int q[], int l, int r) //返回[l, r]区间上的逆序对数量
{
if (l >= r) return 0;
//递归排序
int mid = l + r >> 1;
LL res = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);
//归并过程
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
{
if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
else
{
tmp[k ++ ] = q[j ++ ];
res += mid - i + 1;
}
}
//收尾过程
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];
return res;
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
cout << merge_sort(q, 0, n - 1) << endl;
return 0;
}
3.堆排序#
- 算法内容:
- 时间复杂度:
#include <iostream>
#include <cstring>
#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;
}
4.计数排序#
5.基数排序#
6.冒泡排序#
-
算法内容:
枚举数组中的元素,比较相邻元素,如果左端元素大于有段元素,则交换两个数。这样操作后数组最右端的元素即为该数组中所有元素的最大值。接着对该数组除最右端的
个元素进行同样的操作,再接着对剩下的 个元素做同样的操作,直到整个数组有序排列。 -
时间复杂度:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n, q[N];
void bubble_sort(int q[], int n)
{
for (int i = 0; i < n - 1; i ++) //枚举比较的趟数,一共需要进行n - 1趟
for (int j = 0; j < n - i - 1; j ++) //枚举数列中的元素(除去已经确定最大值的元素)
if (q[j] > q[j + 1])
swap(q[j], q[j + 1]);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++) cin >> q[i];
bubble_sort(q, n);
for (int i = 0; i < n; i ++) cout << q[i] << " ";
return 0;
}
7.选择排序#
-
算法内容:
假定数组最左端元素
最小。接着从他的右边开始遍历,找到它右边最小的数 ,如果 则将 和 交换,直到排序完整个数组。 -
时间复杂度:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n;
int a[N];
void insertion_sort(int a[], int n)
{
int j = 0, key;
for (int i = 1; i < n; i ++ )
{
key = a[i];
j = i - 1;
while ((j >= 0) && (a[j] > key))
{
a[j + 1] = a[j];
j -- ;
}
a[j + 1] = key;
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ ) cin >> a[i];
insertion_sort(a, n);
for (int i = 0; i < n; i ++ ) cout << a[i] << ' ';
return 0;
}
8.插入排序#
-
算法内容:
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。例如要将数组
排序,可以将 看做是一个有序序列,将 看做一个无序序列。无序序列中 比 小,于是将 插入到 的左边,此时有序序列变成了 ,无序序列变成了 。无序序列中 比 大,于是将 插入到 的右边,有序序列变成了 ,无序序列变成了 。以此类推,最终数组按照从小到大排序。 -
时间复杂度:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
void select_sort(int a[], int n)
{
for (int i = 0; i < n; i ++ )
{
int minpos = i; //假定i位置的数最小
for (int j = i + 1; j < n; j ++ )
{
if (a[j] < a[minpos])
{
minpos = j;
}
}
if (minpos != i) swap(a[i], a[minpos]);
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ ) cin >> a[i];
select_sort(a, n);
for (int i = 0; i < n; i ++ ) cout << a[i] << ' ';
return 0;
}
9.希尔排序#
10.桶排序#
2.二分#
- 只要具有二段性,就可以二分。
1.整数二分#
-
模板一
/*当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。*/ int bsearch_1(int l, int r) { while (l < r) { int mid = l + r >> 1; if (check(mid)) r = mid; else l = mid + 1; } return l; }
-
模板二
/*当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。*/ int bsearch_2(int l, int r) { while (l < r) { int mid = l + r + 1 >> 1; if (check(mid)) l = mid; else r = mid - 1; } return l; }
-
模板题
//在有序数组中查找某个数出现的范围(Acwing 789) #include <iostream> using namespace std; const int N = 100010; int q[N]; int n, m; int main() { cin >> n >> m; for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]); while (m -- ) { int x; cin >> x; int l = 0, r = n - 1; //二分左端点 //写法一: // while (l < r) // { // int mid = (l + r) >> 1; // if (q[mid] >= x) r = mid; // else l = mid + 1; // } //写法二: while (l < r) { int mid = (l + r) >> 1; if (q[mid] < x) l = mid + 1; else r = mid; } if (q[l] != x) cout << "-1 -1" << endl; else { cout << l << ' '; l = 0, r = n - 1; //二分右端点 //写法一: // while (l < r) // { // int mid = (l + r + 1) >> 1; // if (q[mid] <= x) l = mid; // else r = mid - 1; // } //写法二: while (l < r) { int mid = (l + r + 1) >> 1; if (q[mid] > x) r = mid - 1; else l = mid; } cout << l << endl; } } return 0; }
2.浮点数二分#
-
模板
//不需要考虑取整问题,更新操作为l = mid, 或r = mid double bsearch(int l, int r) { while (r - l > 1e-8) { double mid = (l + r) / 2.0; if (check(mid)) l = mid; else r = mid; } return l; }
-
模板题
//求浮点数平方根 #include <iostream> #include <algorithm> #include <cstring> using namespace std; int main() { double x; cin >> x; double l = 0, r = x; while (r - l > 1e-8) { double mid = (l + r) / 2; if (mid * mid >= x) r = mid; else l = mid; } printf("%lf\n", l); return 0; }
3. 高精度#
1.高精度加法#
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B) //加引用后不复制数组节省时间
{
vector<int> C;
int t = 0;//进位
for (int i = 0; i < A.size() || i < B.size(); i ++ )
{
if (i < A.size()) t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if (t) C.push_back(1);
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
auto C = add(A, B);
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];
cout << endl;
return 0;
}
2.高精度减法#
#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() - 1; i >= 0; i -- )
if (A[i] != B[i])
return A[i] > B[i];
return true;
}
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size(); i ++ )
{
t = A[i] - t;
if (i < B.size()) t -= B[i];
C.push_back((t + 10) % 10);
if (t < 0) t = 1;
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();//去掉前导0
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
vector<int> C;
if (cmp(A, B)) C = sub(A, B);
else C = sub(B, A), cout << '-';
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];
cout << endl;
return 0;
}
3.高精度乘法#
#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 ++ )
{
if (i < A.size()) t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
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');
auto C = mul(A, b);
for (int i = C.size() - 1; i >= 0; i -- ) printf("%d", C[i]);
return 0;
}
4.高精度除法#
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> div(vector<int> &A, int b, int &r)
{
vector<int> C;
r = 0;
for (int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
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 -- ) cout << C[i];
cout << endl << r << endl;
return 0;
}
4. 前缀和与差分#
1. 一维前缀和#
-
算法内容:
预处理:循环一遍
数组, ,得到前缀和数组 ,查询:
-
时间复杂度:
预处理:
查询:
//求区间和
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N]; //a为原数组,b为前缀和数组
int main()
{
scanf("%d%d", &n, &m);
//读入原数组
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
//前缀和的初始化
for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
//通过查询前缀和数组得到区间和
while (m -- )
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]); // 区间和的计算
}
return 0;
}
2. 二维前缀和#
-
算法内容:
预处理:循环一遍
矩阵,令 ,得到前缀和矩阵 ,此时 表示的是左上角是 右下角是 的矩阵的所有元素的和
查询:左上角是
-
时间复杂度:
预处理:
查询:
//求子矩阵的和
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], s[N][N]; //a为原矩阵,s为前缀和矩阵
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);
printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
return 0;
}
3. 一维差分#
-
算法内容:
差分数组
的定义:在
数组的 区间上都加上一个数 :b[r + 1] -= c, b[l] += c
-
时间复杂度:
预处理:
查询:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N]; //a为原数组,b为差分数组
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
scanf("%d%d", &n, &m);
//读入原数组
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
//初始化差分数组
for (int i = 1; i <= n; i ++ ) insert(i, i, a[i]);
//通过差分数组实现对原数组的增添元素
while (m -- )
{
int l, r, c;
scanf("%d%d%d", &l, &r, &c);
insert(l, r, c);
}
//对差分数组求前缀和
for (int i = 1; i <= n; i ++ ) b[i] += b[i - 1];
//输出操作后的数组
for (int i = 1; i <= n; i ++ ) printf("%d ", b[i]);
return 0;
}
4. 二维差分#
-
算法内容:
预处理:用
函数初始化差分矩阵 ,然后对差分矩阵 求前缀和
查询:
-
时间复杂度:
预处理:
查询:
#include <iostream>
#include <cstring>
#include <algorithm>
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;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main()
{
scanf("%d%d%d", &n, &m, &q);
//读入原矩阵
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
scanf("%d", &a[i][j]);
//初始化差分矩阵
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
insert(i, j, i, j, a[i][j]);
//通过差分矩阵实现对原矩阵的增添元素
while (q -- )
{
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}
//对差分矩阵求前缀和
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
//输出操作后的矩阵
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= m; j ++ )
printf("%d ", b[i][j]);
puts("");
}
return 0;
}
双指针
模板#
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两 个有序序列的操作
例题#
1.输入abc def ghi,输出abc和def和ghi#
#include<iostream>
#include<string.h>
using namespace std;
char str[1000];
int j = 0;
int main()
{
gets(str);
int n = strlen(str);
for (int i = 0; i < n; i ++)//i每次都指向每个单词的第一个字母
{
j = i;
while (j < n && str[j] != ' ')
{
j ++;
}
//这道题的具体逻辑
for (int k = i; k < j; k ++) cout << str[k];
cout << endl;
i = j;
}
return 0;
}
2.AcWing 799. 最长连续不重复子序列#
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int q[N], s[N];
//i指的是每段序列的终点
//j指的的是j向左最远能走多少
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 ++ ]] -- ;
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
位运算
1.统计x的二进制表示里面有多少1#
#include<iostream>
using namespace std;
int lowbit(int x)
{
return (x & - x);
}
int main()
{
int res = 0;
int x = 18; //10010
int tmp = x;
printf("lowbit(x) = %d\n", lowbit(x));
while (x)
{
x -= lowbit(x);
res ++;
}
printf("%d的二进制表示中有%d个1出现\n",tmp, res);
return 0;
}
2.一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数#
#include<iostream>
using namespace std;
const int N = 10010;
int arr[N], n;
void printOddTimesNum1(int *arr, int len)
{
int eor = 0;
for (int i = 0; i < n; i ++)
{
eor = eor ^ arr[i];
}
printf("%d", eor);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++) scanf("%d", &arr[i]);
printOddTimesNum1(arr, n);
return 0;
}
3.一个数组中有俩种数出现了奇数次,#
其他数都出现了偶数次,怎么找到并打印这两种数
#include<iostream>
using namespace std;
const int N = 10010;
int lowbit(int x)
{
return (x & - x);
}
void printOddNum2(int *arr, int len)
{
int eor = 0;
for (int i = 0; i < len; i ++)
{
eor ^= arr[i];
}
//eor = a ^ b
//eor != 0 (因为a 和 b一定不相等,如果相等则a和b出现了偶数次矛盾)
//eor必然有一个位置上是1
// 假设eor = 0110010000
//则lowbit(eor) = 0000010000
int rightone = lowbit(eor); //提取出eor最右的1
int onlyone = 0; //eor'
for (int i = 0; i < len; i ++)
{
if ((arr[i] & rightone) != 0)//arr[i]的那个位置上一定是1,
//所以找出了那个位置上为1的所有数字,也意味着将a 和 b分开了
//此时符合if的arr[i]只可能包含a或者只可能包含b
{
onlyone ^= arr[i];
}
}
printf("%d %d", onlyone, (eor ^ onlyone));
//假设onlyone是a,则eor ^ eor' = a ^ b ^ a = b
}
int main()
{
int n, arr[N] = {0};
cin >> n;
for (int i = 0; i < n; i ++) scanf("%d", &arr[i]);
printOddNum2(arr, n);
return 0;
}
不创建额外变量实现两个数的交换#
#include<iostream>
using namespace std;
int main()
{
int a = 3, b = 6;
a = a ^ b;
b = a ^ b;
a = a ^ b;
cout << a << " " << b;
return 0;
}
离散化
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
//二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ..., n
//或者 return r 映射到0, 1, 2, ..., n
}
-
例题
区间和
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N];
vector<int> alls;
vector<PII> add, query;
int find(int x)
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 0; i < m; i ++ )
{
int l, r;
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
// 去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
// 处理插入
for (auto item : add)
{
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];
// 处理询问
for (auto item : query)
{
int l = find(item.first), r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
区间合并
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10, INF = 2e9;
int n;
vector<PII> segs;
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end()); //按照区间的左端点排序
int st = -INF, ed = -INF; //维护当前区间
for (auto seg : segs) //枚举当前区间的下一个区间
{
//当前区间和下一个区间没有重叠,将当前区间加到答案里
//并且将当前区间更新为下一个区间
if (ed < seg.first)
{
if (st != -INF) res.push_back(seg);
st = seg.first, ed = seg.second;
}
//当前区间和下一个区间有重叠,将当前区间的右端点更新为较大的右端点,
//实现区间合并,并准备枚举下一个区间
else
{
ed = max(ed, seg.second);
}
}
//将最后一个区间加到答案里
if (st != -INF) res.push_back({st, ed});
segs = res;
}
int main()
{
cin >> n;
while (n -- )
{
int l, r;
cin >> l >> r;
segs.push_back({l, r});
}
merge(segs);
cout << segs.size() << endl;
return 0;
}
KMP
-
写法一
#include<iostream> #include<cstring> #include<cstdio> using namespace std; const int N = 1000; const int M = 100; int ne[M] = {0}; //求ne数组 O(M) void getne(char ms[], int ne[]) { int len = strlen(ms); if (len == 1) ne[0] = - 1; ne[0] = - 1; ne[1] = 0; int i = 2;//next数组的位置 int cn = 0; while (i < len) { if (ms[i - 1] == ms[cn]) ne[i ++] = ++ cn; else if (cn > 0) //当前跳到cn的位置,和i - 1位置的字符配不上 cn = ne[cn]; else ne[i ++] = 0; } } //匹配过程 O(N) int getindex(char s[], char p[], int res) { int i1 = res; int i2 = 0; int len1 = strlen(s); int len2 = strlen(p); if (s == "" || p == "" || len1 < len2) return - 1; for (i1 = res) while (i1 < len1 && i2 < len2) //都不越界 { if(s[i1] == p[i2]) { i1 ++; i2 ++; } else if(ne[i2] == - 1) i1 ++; else i2 = ne[i2];//回跳 } //i1越界或者i2越界 return i2 == len2 ? (i1 - i2) : - 1; } int main() { int start = 0; char s[N]; char p[M]; scanf("%s", s); scanf("%s", p); getne(p, ne); //for (int i = 0; i < strlen(m); i ++) printf("%d", ne[i]); while ( (start = getindex(s, p, start)) != - 1) { printf("%d ", getindex(s, p, start++)); } if (getindex(s, p, start) == - 1) printf("Not Found\n"); return 0; }
-
写法二
#include <iostream> using namespace std; const int N = 100010, M = 1000010; int n, m; //s串的长度为m, p串的长度为n int ne[N]; char s[M], p[N]; int main() { cin >> n >> p + 1 >> m >> s + 1; //求next数组 (ne[0] = 0, 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 + 1 - 1); j = ne[j]; } } return 0; }
基础数据结构
1.单链表#
#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m -- )
{
int k, x;
char op;
cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
2.双链表#
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
int main()
{
cin >> m;
// 0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
while (m -- )
{
string op;
cin >> op;
int k, x;
if (op == "L")
{
cin >> x;
insert(0, x);
}
else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "D")
{
cin >> k;
remove(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else
{
cin >> k >> x;
insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
3.栈#
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int stk[N], tt;
int main()
{
cin >> m;
while (m -- )
{
string op;
int x;
cin >> op;
if (op == "push")
{
cin >> x;
stk[ ++ tt] = x;
}
else if (op == "pop") tt -- ;
else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
else cout << stk[tt] << endl;
}
return 0;
}
单调栈#
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt -- ;
if (!tt) printf("-1 ");
else printf("%d ", stk[tt]);
stk[ ++ tt] = x;
}
return 0;
}
4.队列#
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int q[N], hh, tt = -1;
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;
else cout << q[hh] << endl;
}
return 0;
}
单调队列#
#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;
for (int i = 0; i < n; i ++ )
{
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
q[ ++ tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
q[ ++ tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
return 0;
}
5.Trie树#
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;
char str[N];
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++ ;
}
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
char op[2];
scanf("%s%s", op, str);
if (*op == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}
6.并查集#
-
支持的操作
-
将两个集合合并
-
询问两个元素是否在一个集合当中
-
-
基本原理
每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点
-
问题
- 如何判断树根:if (p[x] == x)
- 如何求x的集合编号:while (p[x] != x) x = p[x];
- 如何合并两个集合:px是x的集合编号,py是y的集合编号。p[px] = py;
-
优化:路径压缩
#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];
int a, b;
scanf("%s%d%d", op, &a, &b);
if (*op == 'M') p[find(a)] = find(b);
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
7.堆#
-
定义:用一维数组存储小根堆(完全二叉树),对于节点x,它的左儿子是2x,右儿子是2x+1
-
支持的操作
- 插入一个数
:heap[++ size] = x; up(size);
- 求集合当中的最小值
:heap[1];
- 删除最小值
:heap[1] = heap[size]; size --; down(1);
- 删除下标为k的元素
:heap[k] = heap[size]; size --; down(k); up(k);
(优先队列无法实现,想要修改某个元素只能向堆里插入这个元素) - 修改下标为k的元素
:heap[k] = x; down(k); up(k);
(优先队列无法实现,想要修改某个元素只能向堆里插入这个元素)
- 插入一个数
-
up函数
void up(int u) { while (u / 2 && h[u / 2] > h[u]) //有父节点并且当前点比父节点更小 { swap(h[u / 2], h[u]); u /= 2; } }
-
down函数
void down(int u) { int t = u; //t为三个数中最小的数的编号 if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2; //如果左儿子存在并且左儿子比当前点小 if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; //如果右儿子存在并且右儿子比当前点小 if (u != t) //根节点不是三个数中最小的 { swap(h[t], h[u]); down(t); } }
1. 堆排序#
#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);
//O(n)
while (m -- )
{
printf("%d ", h[1]);
h[1] = h[cnt -- ];
down(1);
}
puts("");
return 0;
}
2. 模拟堆#
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
int h[N], ph[N], hp[N], cnt;
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
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)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
int main()
{
int n, m = 0;
scanf("%d", &n);
while (n -- )
{
char op[5];
int k, x;
scanf("%s", op);
if (!strcmp(op, "I"))
{
scanf("%d", &x);
cnt ++ ;
m ++ ;
ph[m] = cnt, hp[cnt] = m;
h[cnt] = x;
up(cnt);
}
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
else if (!strcmp(op, "DM"))
{
heap_swap(1, cnt);
cnt -- ;
down(1);
}
else if (!strcmp(op, "D"))
{
scanf("%d", &k);
k = ph[k];
heap_swap(k, cnt);
cnt -- ;
up(k);
down(k);
}
else
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
up(k);
down(k);
}
}
return 0;
}
8.哈希表#
1. 拉链法#
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100003;//质数可以降低冲突概率
int h[N], e[N], ne[N], idx;
void insert(int x)
{
int k = (x % N + N) % N;//哈希函数
//头插法插入一个数,把h[k]看成head头指针,
//h[k]=idx++的原因是由于头插法导致头指针在不断改变.
//从后往前赋值
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
bool find(int x)
{
int k = (x % N + N) % N;//保证k为正数
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);
while (n -- )
{
char op[2];//即使读入1个字符也应该用字符串存储,因为scanf读入字符串时自动忽略空格换行制表符等
int x;
scanf("%s%d", op, &x);
if (*op == 'I') insert(x);
else
{
if (find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
2. 开放寻址法#
#include <cstring>
#include <iostream>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;//一个大于10的九次方的数
int h[N];
//如果x在哈希表中已经存在,返回x的位置;
//如果x在哈希表中不存在,返回x应该存储的位置
int find(int x)
{
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x)
{
k ++ ;
if (k == N) k = 0;
}
return k;
}
int main()
{
memset(h, 0x3f, sizeof h);//按字节将所有位置初始化为空
int n;
scanf("%d", &n);
while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
if (*op == 'I') h[find(x)] = x;
else
{
if (h[find(x)] == null) puts("No");
else puts("Yes");
}
}
return 0;
}
3. 字符串哈希#
//字符串前缀哈希法
#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ULL;
//用ULL存储会溢出等价于模上2 ^ 64
const int N = 100010, P = 131;
int n, m;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];//[l, r]区间内字符串的哈希值
}
//哈希方式:把每一个字符串看成一个P进制的数然后模Q,实现了把任何一个字符串映射到从0到Q - 1之间的数
//注意不能映射为0,并且假设不会出现冲突情况(P = 131或13331, Q = 2 ^ 64)
int main()
{
scanf("%d%d", &n, &m);
scanf("%s", str + 1);
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i] - 'a';//预处理前缀哈希值
p[i] = p[i - 1] * 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;
}
高级数据结构
1. 树状数组#
- 支持的操作:
- 在某个位置加上一个数(单点修改)
- 求某一个前缀和(区间查询)
- 在某个位置加上一个数(单点修改)
-
初始化
for (int i = 1; i <= n; i ++ ) add(i, a[i]); //初始化树状数组
-
单点修改
void add(int x, int v) //在树状数组的x位置上加上v { for (int i = x; i <= n; i += lowbit(i)) { tr[i] += v; } }
-
区间查询
int query(int x) //查询x位置的前缀和 { int res = 0; for (int i = x; i > 0; i -= lowbit(i)) { res += tr[i]; } return res; }
-
lowbit操作
int lowbit(int x) //返回二进制的x中的最后一位一,返回值为十进制 { return x & (-x); }
2. 线段树#
-
存储
struct Node { int l, r; int sum; }tr[N * 4];
-
用子节点信息更新当前节点信息
void pushup(int u) { tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; }
-
在一段区间上初始化线段树
void build(int u, int l, int r) { if (l == r) tr[u] = {l, r, w[r]}; else { tr[u] = {l, r}; int mid = l + r >> 1; build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); pushup(u); } }
-
查询
int query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum; int mid = tr[u].l + tr[u].r >> 1; int sum = 0; if (l <= mid) sum += query(u << 1, l, r); if (r > mid) sum += query(u << 1 | 1, l, r); return sum; }
-
修改
void modify(int u, int x, int v) { if (tr[u].l == tr[u].r) tr[u].sum += v; else { int mid = tr[u].l + tr[u].r >> 1; if (x <= mid) modify(u << 1, x, v); else modify(u << 1 | 1, x, v); pushup(u); } }
STL
1.vector#
DFS
- 指数型枚举
#include <iostream>
using namespace std;
const int N = 16;
int n;
int st[N]; // 状态,记录每个位置当前的状态:0表示还没考虑,1表示选它,2表示不选它
void dfs(int u)
{
if (u > n)
{
for (int i = 1; i <= n; i ++ )
{
if (st[i] == 1)
{
cout << i << ' ';
}
}
puts("");
return;
}
st[u] = 2;
dfs(u + 1); // 第一个分支:不选
st[u] = 0; // 恢复现场
st[u] = 1;
dfs(u + 1); // 第二个分支:选
st[u] = 0;
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
- 排列型枚举
#include <iostream>
using namespace std;
const int N = 10;
int n;
int st[N]; // 0 表示还没放数,1~n表示放了哪个数
bool used[N]; // true表示用过,false表示还未用过
void dfs(int u)
{
if (u > n) // 边界
{
//枚举n个位置
for (int i = 1; i <= n; i ++ ) cout << st[i] << ' '; // 打印方案
puts("");
return;
}
// 依次枚举每个分支,即当前位置可以填哪些数
for (int i = 1; i <= n; i ++ ) //枚举1~n的所有数
{
if (!used[i])
{
st[u] = i;
used[i] = true;
dfs(u + 1);
// 恢复现场
st[u] = 0;
used[i] = false;
}
}
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
- 组合型枚举
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 30;
int n, m;
int way[N];
//为了避免重复,保持升序选择,只需保证每次新加的数大于前面的数
void dfs(int u, int start)
{
if (u + n - start < m) return; // 剪枝
if (u == m + 1)
{
for (int i = 1; i <= m; i ++ ) printf("%d ", way[i]);
puts("");
return;
}
for (int i = start; i <= n; i ++ )
{
way[u] = i;
dfs(u + 1, i + 1);
way[u] = 0; // 恢复现场
}
}
int main()
{
scanf("%d%d", &n, &m);
dfs(1, 1);
return 0;
}
BFS
- 例题
- AcWing 844. 走迷宫
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N], d[N][N];
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});
}
}
}
return d[n - 1][m - 1];
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
或者
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int n, m;
PII q[N * N];
int g[N][N];
int d[N][N];
int bfs()
{
int hh = 0, tt = -1;
q[ ++ tt] = {0, 0};
memset(d, -1, sizeof d);
d[0][0] = 0;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, -0, -1};
while (hh <= tt)
{
auto t = q[hh ++]; //取出队头, 并弹出队头
for (int i = 0; i < 4; i ++ )
{
int x = t.first + dx[i], y = t.second + dy[i];
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[ ++ tt] = {x, y}; //新元素入队
}
}
}
return d[n - 1][m - 1];
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
图论
- 无向图是有两个方向的有向图,树是无环有向连通图
1. 图的存储#
- 稀疏图用邻接表
- 稠密图用邻接矩阵
(邻接表)
#include <iostream>
using namespace std;
int h[N], e[N], ne[N], idx;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
int main()
{
memset(h, -1, sizeof h);
return 0;
}
2. 图的深度优先遍历#
-
模板
bool st[N]; int h[N], e[N], ne[N], idx; void dfs(int u) //u表示正在搜的点的编号 { st[u] = true; //表示点u已经被搜过了 for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) dfs(j); } }
-
例题
1.AcWing 846. 树的重心
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 100010; int n; int ans = N; int h[N], e[N * 2], ne[N * 2], idx; bool st[N]; void add(int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx ++; } //返回以u为根的子树中点的数量 int dfs(int u) { st[u] = true; int sum = 1, res = 0; for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) { int s = dfs(j); res = max(res, s); sum += s; } } res = max(res, n - sum); ans = min(ans, res); return sum; } int main() { cin >> n; memset(h, -1, sizeof h); for (int i = 0; i < n - 1; i ++ ) { int a, b; cin >> a >> b; add(a, b), add(b, a); } dfs(1); cout << ans << endl; return 0; }
3. 图的广度优先遍历#
-
例题
- AcWing 847. 图中点的层次
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <queue> using namespace std; const int N = 100010; int n, m; int h[N], e[N], ne[N], idx; int d[N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } int bfs() { 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] = d[t] + 1; q.push(j); } } } return d[n]; } int main() { scanf("%d%d", &n, &m); memset(h, -1, sizeof h); for (int i = 0; i < m; i ++ ) { int a, b; scanf("%d%d", &a, &b); add(a, b); } cout << bfs() << endl; return 0; }
4. 有向图的拓扑序列#
-
有向无环图一定存在拓扑序列,被称为拓扑图
-
一个有向无环图一定至少存在一个入度为0的点
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int e[N], ne[N], idx;//邻接表存储图
int h[N];
int q[N], hh = 0, tt = -1;//队列保存入度为0的点,也就是能够输出的点,
int n, m;//保存图的点数和边数
int d[N];////保存各个点的入度
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void topsort(){
for(int i = 1; i <= n; i++){//遍历一遍顶点的入度。
if(d[i] == 0)//如果入度为 0, 则可以入队列
q[++tt] = i;
}
while(tt >= hh){//循环处理队列中点的
int a = q[hh++];
for(int i = h[a]; i != -1; i = ne[i]){//循环删除 a 发出的边
int b = e[i];//a 有一条边指向b
d[b]--;//删除边后,b的入度减1
if(d[b] == 0)//如果b的入度减为 0,则 b 可以输出,入队列
q[++tt] = b;
}
}
if(tt == n - 1){//如果队列中的点的个数与图中点的个数相同,则可以进行拓扑排序
for(int i = 0; i < n; i++){//队列中保存了所有入度为0的点,依次输出
cout << q[i] << " ";
}
}
else//如果队列中的点的个数与图中点的个数不相同,则不可以进行拓扑排序
cout << -1;//输出-1,代表错误
}
int main(){
cin >> n >> m;//保存点的个数和边的个数
memset(h, -1, sizeof h);//初始化邻接矩阵
while (m -- ){//依次读入边
int a, b;
cin >> a >> b;
d[b]++;//顶点b的入度+1
add(a, b);//添加到邻接矩阵
}
topsort();//进行拓扑排序
return 0;
}
5. 最短路#
1. 单源最短路#
1. 所有边权都是正数#
-
朴素Dijkstra算法
(稠密图)//注意存在重边和自环,但是因为所有边权都是正数, //所以自环一定不会出现在最短路之中,重边只需要保留一条最短的边 #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; //每迭代一次就会确定一个点的最短路,迭代n次后就会找到所有点的最短路 for (int i = 0; i < n; i ++ ) { //找到所有还未确定最短路的点中的到起点距离最小的点 int t = -1; for (int j = 1; j <= n; j ++ ) //枚举所有点 if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; if (t == n) break; //点t已经被确定 st[t] = true; //用t更新其他点 for (int j = 1; j <= n; j ++ ) //枚举所有点 dist[j] = min(dist[j], dist[t] + g[t][j]); } if (dist[n] == 0x3f3f3f3f) return -1; //1和n不连通,不存在最短路 return dist[n]; } int main() { cin >> n >> m; memset(g, 0x3f, sizeof g); while (m -- ) { int a, b, c; scanf("%d%d%d", &a, &b, &c); g[a][b] = min(g[a][b], c);//重边只需要保留一条最短的边 } cout << dijkstra() << endl; return 0; }
-
堆优化Dijkstra算法
(稀疏图)- 用堆优化了每一次迭代时找最小距离的点的操作,将这一步从
优化为
#include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std; typedef pair<int, int> PII; //<距离,编号> const int N = 1000010; int n, m; int h[N], e[N], w[N], ne[N], idx; int dist[N]; bool st[N]; void add(int a, int b, int c) { e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++ ; } int dijkstra() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; priority_queue<PII, vector<PII>, greater<PII>> heap; //小根堆 heap.push({0, 1}); //距离是0,编号为1 while (heap.size()) { auto t = heap.top(); //找到最小 heap.pop(); int ver = t.second, distance = t.first; if (st[ver]) continue; //当前这个点是冗余备份,没必要再操作 st[ver] = true; //用t更新其他点 for (int i = h[ver]; i != -1; i = ne[i]) //枚举t的下一层 { int j = e[i]; if (dist[j] > dist[ver] + w[i]) { dist[j] = dist[ver] + w[i]; heap.push({dist[j], j}); //用添加元素的方式实现修改堆里的某个元素,会造成冗余 } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; } int main() { cin >> n >> m; memset(h, -1, sizeof h); while (m -- ) { int a, b, c; scanf("%d%d%d", &a, &b, &c); add(a, b, c); } cout << dijkstra() << endl; return 0; }
- 用堆优化了每一次迭代时找最小距离的点的操作,将这一步从
2. 存在负权边#
-
bellman-ford
//如果在1~n的路径中存在负权回路,则不存在最短路 #include <iostream> #include <algorithm> #include <cstring> using namespace std; const int N = 510, M = 10010; int n, m, k; int dist[N], backup[N]; struct edge { int a, b, w; }edges[M]; int bellman_ford() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; for (int i = 0; i < k; i ++ ) { memcpy(backup, dist, sizeof dist); //防止串联 for (int j = 0; j < m; j ++ ) { int a = edges[j].a, b = edges[j].b, w = edges[j].w; dist[b] = min(dist[b], backup[a] + w); } } if (dist[n] > 0x3f3f3f3f / 2) return -0x3f3f3f3f; return dist[n]; } int main() { cin >> 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) puts("impossible"); else cout << t << endl; return 0; }
-
spfa 一般
,最坏#include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std; typedef pair<int, int> PII; //<距离,编号> const int N = 1000010; int n, m; int h[N], e[N], w[N], ne[N], idx; int dist[N]; bool st[N]; void add(int a, int b, int c) { e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++ ; } int spfa() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; queue<int> q; q.push(1); st[1] = true; while (q.size()) { int t = q.front(); q.pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; if (!st[j]) { q.push(j); st[j] = true; } } } } if (dist[n] == 0x3f3f3f3f) return -0x3f3f3f3f; return dist[n]; } int main() { cin >> 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 cout << t << endl; return 0; }
-
spfa判断负环
#include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std; typedef pair<int, int> PII; //<距离,编号> const int N = 1000010; int n, m; int h[N], e[N], w[N], ne[N], idx; int dist[N], cnt[N]; //cnt[]表示当前点所对应的最短路径的有多少条边 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() { queue<int> q; //初始把所有点放到队列里 for (int i = 1; i <= n; i ++ ) { q.push(i); st[i] = 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]; cnt[j] = cnt[t] + 1; //边数>=n时说明有n + 1个点说明有负环 if (cnt[j] >= n) return true; if (!st[j]) { q.push(j); st[j] = true; } } } } return false; } int main() { cin >> n >> m; memset(h, -1, sizeof h); while (m -- ) { int a, b, c; scanf("%d%d%d", &a, &b, &c); add(a, b, c); } if (spfa()) puts("Yes"); else puts("No"); return 0; }
2. 多源汇最短路#
floyd
#include <iostream>
#include <algorithm>
#include <cstring>
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()
{
cin >> 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, w;
scanf("%d%d%d", &a, &b, &w);
d[a][b] = min(d[a][b], w); //重边
}
floyd();
while (q -- )
{
int a, b;
scanf("%d%d", &a, &b);
if (d[a][b] > INF / 2) puts("impossible");
else printf("%d\n", d[a][b]);
}
return 0;
}
6.最小生成树#
-
朴素版prim
(稠密图)#include <iostream> #include <algorithm> #include <cstring> using namespace std; const int N = 510, INF = 0x3f3f3f3f; int n, m; int g[N][N]; int dist[N]; bool st[N]; int prim() { memset(dist, 0x3f, sizeof dist); int res = 0; for (int i = 0; i < n; i ++ ) { //找到集合外到当前集合距离最近的点 int t = -1; for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; if (i && dist[t] == INF) return INF; if (i) res += dist[t]; //用t更新其他点到集合的距离 for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]); st[t] = true; } return res; } int main() { cin >> n >> m; memset(g, 0x3f, sizeof g); while (m -- ) { int a, b, w; scanf("%d%d%d", &a, &b, &w); g[a][b] = g[b][a] = min(g[a][b], w); //重边 } int t = prim(); if (t == INF) puts("impossible"); else cout << t << endl; return 0; }
-
堆优化版的prim
(稀疏图) -
kruskal
#include <iostream> #include <algorithm> #include <cstring> using namespace std; const int N = 200010; int n, m; int p[N]; struct Edge { int a, b, w; bool operator < (const Edge &t) const { return w < t.w; } }edges[N]; int find(int x) //返回x的祖宗节点 { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int main() { cin >> n >> m; for (int i = 0; i < m; i ++ ) { int a, b, w; scanf("%d%d%d", &a, &b, &w); edges[i] = {a, b, w}; } //把所有边排序 sort(edges, edges + m); //初始化并查集 for (int i = 1; i <= n; i ++ ) p[i] = i; //从小到大枚举所有边 int res = 0, cnt = 0; for (int i = 0; i < m; i ++ ) { int a = edges[i].a, b = edges[i].b, w = edges[i].w; if (find(a) != find(b)) //如果不连通 { p[find(a)] = find(b); //将这条边加入集合中 res += w; cnt ++ ; } } if (cnt < n - 1) puts("impossible"); else cout << res << endl; return 0; }
7. 二分图#
1.染色法判定二分图
- 性质:一个图是二分图当且仅当它不含奇数环
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int h[N], e[M], ne[M], idx;
int color[N]; //0表示未染色,1表示1号色,2表示2号色
void add(int a, int b) //a -> b
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool dfs(int u, int c) //u号点,颜色为c。返回false表示染色发生矛盾
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!color[j]) //如果当前点没染过颜色,就染成与u号点不同的颜色
{
//3 - c 表示 1变2,2变1
if (!dfs(j, 3 - c)) return false; //染色发生矛盾
}
//如果当前点已经染过颜色,直接判断它的颜色与u号点的颜色是否相同
else if (color[j] == c) return false;
}
return true;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a); //无向图
}
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (!color[i]) //如果没染色就染成1号色
{
if (!dfs(i, 1)) //如果染色发生矛盾
{
flag = false;
break;
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
2.匈牙利算法
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N]; //女生匹配的男生是谁, 是0表示没有匹配
bool st[N]; //判重
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i]) //枚举男生x喜欢的女生
{
int j = e[i];
if (!st[j])
{
st[j] = true;
//这个女生没有匹配或者她匹配的男生能找到下家
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
cin >> n1 >> n2 >> m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
int res = 0;
for (int i = 1; i <= n1; i ++ ) //枚举所有男生
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
cout << res << endl;
return 0;
}
数学知识
1. 质数#
- 质数的定义:在大于1的整数中,如果只包含1和本身这两个约数,就被称为质数,或者叫素数。
- 质因数的定义:能整除给定正整数的质数。
-
质数的判定
//试除法 O(sqrt(n)) //因为 如果d | n,则n/d | n,所以只需枚举数值小的,即d < d/n bool is_prime(int n) { if (n < 2) return false; for (int i = 2; i <= n / i; i ++ ) if (n % i == 0) return false; return true; }
-
分解质因数
//试除法 O(sqrt(n)) void divide(int x) { for (int i = 2; i <= x / i; i ++ ) { //n不包含任何从2到i-1之间的质因子(已经被除干净了),所以i也不包含何从2到i-1之间的质因子 if (x % i == 0) //i一定是质数 { int s = 0; while (x % i == 0) { x /= i; s ++ ; } cout << i << ' ' << s << endl; } } if (x > 1) cout << x << ' ' << 1 << endl;//n中最多只包含一个大于sqrt(n)的质因子 puts(""); }
-
筛质数
-
埃氏筛法
int primes[N], cnt; bool st[N]; void get_primes(int n) { for (int i = 2; i <= n; i ++ ) { if (!st[i]) primes[cnt ++] = i; //筛掉所有合数的倍数 for (int j = i + i; j <= n; j += i) st[j] = true; } }
-
线性筛法
//每个数都会被它的最小质因子筛掉,并且每个数只有一个最小质因子,所以只会被筛一次,所以是线性的 //任何一个合数一定会被筛掉。因为对于一个合数x,假设primes[j]是x的最小质因子,当i枚举到x/primes[j]时,x会被筛掉 void get_primes(int n) { for (int i = 2; i <= n; i ++ ) { if (!st[i]) primes[cnt ++] = i; for (int j = 0; primes[j] <= n / i; j ++ ) //从小到大枚举所有质数 { st[primes[j] * i] = true; //prime[j]一定是primes[j] * i的最小质因子。 //当i % primes[j] == 0时, primes[j]一定是i的最小质因子, 也一定是primes[j] * i的最小质因子。 //当i % primes[j] != 0时, primes[j]一定小于i的所有质因子, 也一定是primes[j] * i的最小质因子 if (i % primes[j] == 0) break; } } }
-
2. 约数#
-
试除法求一个数的所有约数
// O(sqrt(n)) vector<int> get_divisors(int n) { vector<int> res; for (int i = 1; i <= n / i; i ++ ) { if (n % i == 0) { res.push_back(i); if (i != n / i) res.push_back(n / i); } } sort(res.begin(), res.end()); return res; }
-
约数个数
#include <iostream> #include <algorithm> #include <cstring> #include <unordered_map> using namespace std; typedef long long LL; const int mod = 1e9 + 7; int main() { int n; cin >> n; unordered_map<int, int> primes; while (n -- ) { int x; cin >> x; for (int i = 2; i <= x / i; i ++ ) while (x % i == 0) { x /= i; primes[i] ++ ; } if (x > 1) primes[x] ++ ; } LL res = 1; for (auto prime : primes) res = res * (prime.second + 1) % mod; cout << res << endl; return 0; }
-
约数之和
#include <iostream> #include <algorithm> #include <cstring> #include <unordered_map> using namespace std; typedef long long LL; const int mod = 1e9 + 7; int main() { int n; cin >> n; unordered_map<int, int> primes; while (n -- ) { int x; cin >> x; for (int i = 2; i <= x / i; i ++ ) while (x % i == 0) { x /= i; primes[i] ++ ; } if (x > 1) primes[x] ++ ; } LL res = 1; for (auto prime : primes) { int p = prime.first, a = prime.second; LL t = 1; while (a -- ) t = (t * p + 1) % mod; res = res * t % mod; } cout << res << endl; return 0; }
-
求最大公约数(欧几里得算法,也叫辗转相除法)
//b不是0的时候a和b的最大公约数为gcd(b, a % b) //b是0的时候a和b的最大公约数是a int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
3. 欧拉函数#
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int main()
{
int n;
cin >> n;
while (n -- )
{
int a;
cin >> a;
int res = a;
for (int i = 2; i <= a / i; i ++ )
if (a % i == 0)
{
res = res / i * (i - 1);
while (a % i == 0) a /= i;
}
if (a > 1) res = res / a * (a - 1);
cout << res << endl;
}
return 0;
}
//筛法求欧拉函数
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1000010;
int primes[N], cnt;
int phi[N];
bool st[N];
LL get_eulers(int n)
{
phi[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i])
{
primes[cnt ++ ] = i;
phi[i] = i - 1;
}
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0)
{
phi[primes[j] * i] = phi[i] * primes[j];
break;
}
phi[primes[j] * i] = phi[i] * (primes[j] - 1);
}
}
LL res = 0;
for (int i = 1; i <= n; i ++ ) res += phi[i];
return res;
}
int main()
{
int n;
cin >> n;
cout << get_eulers(n) << endl;
return 0;
}
4. 快速幂#
- (a * a) % p = (a % p) * (b % p) % p
LL qmi(int a, int k, int p)
{
LL res = 1 % p; //k = 0, p = 1时输出结果应该为0
while (k)
{
if (k & 1) res = res * a % p;
k >>= 1;
a = (LL)a * a % p;
}
return res;
}
5. 扩展欧几里得算法#
6. 中国剩余定理#
7. 高斯消元#
8. 求组和数#
-
递推
#include <iostream> #include <algorithm> using namespace std; const int N = 2010, mod = 1e9 + 7; int c[N][N]; void init() { for (int i = 0; i < N; i ++ ) for (int j = 0; j <= i; j ++ ) if (!j) c[i][j] = 1; else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod; } int main() { int n; init(); scanf("%d", &n); while (n -- ) { int a, b; scanf("%d%d", &a, &b); printf("%d\n", c[a][b]); } return 0; }
-
9. 容斥原理#
10. 博弈论#
DP
1. 背包问题#
1. 01背包问题#
-
状态表示:
- 集合:f(i, j)表示只考虑前i个物品,且总体积不大于j的所有选法。
- 属性:最大价值
-
集合划分:第i个物品选或者不选(第i个物品选0个和1个)
不选第i个物品:f[i - 1, j]
选第i个物品:f[i - 1, j - v[i]] + w[i] (去掉第i个物品之后不影响最大值)
//朴素做法
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
/*
滚动数组优化关键在于看是否已经循环过,如果已经循环过,则代表已经被更新,所以是第i层;没更新过的就是第i - 1层
对于方程f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]) 只需要保存第i - 1层的结果,由于j - v[i] < j,所以如果从小到大循环,则f[j - v[i]]在第i层已经被更新过,保存的结果并不是第i - 1层,所以采取从大到小循环体积
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
2. 完全背包问题#
-
状态表示:
- 集合:f(i, j)表示只考虑前i个物品,且总体积不大于j的所有选法。
- 属性:最大价值
-
集合划分:第i个物品选多少个。0个,1个,2个,3个,4个,... ,k个,用k枚举(装k个第i个物品不超过背包体积)
//朴素做法(TLE)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k * v[i] <= j; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
/*
对方程进行变形优化时间
f[i, j] = max(f[i - 1, j], f[i - 1, j - v] + w, f[i - 1, j - 2v] + 2w, f[i - 1, j - 3v] + 3w + ...)
f[i, j - v] = max(f[i - 1, j - v],f[i - 1, j - 2v] + w, f[i - 1, j - 3v] + 2w + ...)
所以 f[i, j] = max(f[i - 1, j], f[i, j - v] + w)
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
/*
优化空间
f[i][j] = max(f[i - 1][j], f[i][j - v] + w)中j - v < j,从小到大循环体积,所以f[i][j - v] 先被更新,代表第i层结果
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = v[i]; j <= m; j ++ )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
3. 多重背包问题#
- 状态表示:
- 集合:f(i, j)表示只考虑前i个物品,且总体积不大于j的所有选法。
- 属性:最大价值
- 集合划分:第i个物品选多少个。0个,1个,2个,3个,4个,... ,s[i]个,用k枚举(装k个第i个物品不超过背包体积)
//朴素做法
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i ++ )
{
for (int j = 0; j <= m; j ++ )
{
for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
{
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
//空间优化
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int f[N], v[N], w[N], s[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= 0; j -- )
for (int k = 0; k <= s[i] && j >= k * v[i]; k ++ )
f[j] = max(f[j], f[j - k * v[i]] + w[i] * k);
cout << f[m] << endl;
return 0;
}
/*
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
*/
/*
无法对方程进行类似完全背包问题的时间优化,所以进行二进制优化,实现了将时间复杂度从NVS优化为NVlogs
优化过程:
将每一层i中的s个物品打包为1, 2, 4, 8, 16, 32, 64, ...
含义是第i个物品选1个, 第i个物品选2个, 第4个物品选8个...
对打包后的物品进行01选择可以拼凑出所有的s个物品
例如:
s = 1023
打包为1, 2, 4, 8, ..., 512
s = 200
打包为1, 2, 4, 8, 16, 32, 64, 73
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int f[M];
int main()
{
cin >> n >> m;
int cnt = 0; //记录产生的新的物品的编号
for (int i = 1; i <= n; i ++ )
{
int a, b, s;
cin >> a >> b >> s;
//每次将k个第i个物品打包在一起
int k = 1;
while (k <= s)
{
cnt ++ ;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if (s > 0) //还剩下一些物品需要补上
{
cnt ++ ;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
//转化为01背包问题
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
/*
0<N≤1000
0<V≤20000
0<vi,wi,si≤20000
*/
/*
进行单调队列优化
*/
4. 混合背包问题#
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1010;
int n, m;
int f[N];
struct Thing
{
int kind;
int v, w;
};
vector<Thing> things;
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int v, w, s;
cin >> v >> w >> s;
if (s < 0) things.push_back({-1, v, w}); //01背包
else if (s == 0) things.push_back({0, v, w}); //完全背包
else //多重背包问题的二进制优化转化为01背包
{
for (int k = 1; k <= s; k ++ )
{
s -= k;
things.push_back({-1, v * k, w * k});
}
if (s > 0) things.push_back({-1, v * s, w * s});
}
}
for (auto thing : things)
{
if (thing.kind < 0) //01背包,从大到小循环
{
for (int j = m; j >= thing.v; j -- )
f[j] = max(f[j], f[j - thing.v] + thing.w);
}
else //完全背包,从小到大循环
{
for (int j = thing.v; j <= m; j ++ )
f[j] = max(f[j], f[j - thing.v] + thing.w);
}
}
cout << f[m] << endl;
return 0;
}
5. 二维费用的背包问题#
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int n, v, m;
int f[N][N];
int main()
{
cin >> n >> v >> m;
for (int i = 0; i < n; i ++ )
{
int a, b, c;
cin >> a >> b >> c; //v[i], m[i], w[i]
for (int j = v; j >= a; j -- )
for (int k = m; k >= b; k -- )
f[j][k] = max (f[j][k], f[j - a][k - b] + c);
}
cout << f[v][m] << endl;
return 0;
}
6. 分组背包问题#
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int n, m;
int f[N], v[N], w[N];
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int s;
cin >> s;
for (int j = 0; j < s; j ++ ) cin >> v[j] >> w[j];
for (int j = m; j >= 0; j -- )
for (int k = 0; k < s; k ++ )
if (j >= v[k])
f[j] = max(f[j], f[j - v[k]] + w[k]);
}
cout << f[m] << endl;
return 0;
}
7. 背包问题求方案数#
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, INF = 1e6, mod = 1e9 + 7;
int n, m;
int f[N], g[N]; //f[j]表示体积为j的最大价值,g[j]表示体积为j的方案数
int main()
{
cin >> n >> m;
g[0] = 1; //体积为0的方案数为1
for (int i = 1; i <= m; i ++ ) f[i] = -INF; //体积为0的最大价值不存在
for (int i = 0; i < n; i ++ )
{
int v, w;
cin >> v >> w;
for (int j = m; j >= v; j -- )
{
int t = max(f[j], f[j - v] + w); //记录最优解
int s = 0;
//找到由谁转移而来,加上其方案数
if (t == f[j]) s += g[j];
if (t == f[j - v] + w) s += g[j - v];
if (s >= mod) s -= mod;
f[j] = t;
g[j] = s;
}
}
//找到最优解
int maxw = 0;
for (int i = 0; i <= m; i ++ ) maxw = max(maxw, f[i]);
//求方案数
int res = 0;
for (int i = 0; i <= m; i ++ )
{
if (f[i] == maxw)
{
res += g[i];
if (res >= mod) res -= mod;
}
}
cout << res << endl;
return 0;
}
8. 背包问题求具体方案#
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = n; i >= 1; i -- )
for (int j = 0; j <= m; j ++ )
{
f[i][j] = f[i + 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
}
int j = m;
for (int i = 1; i <= n; i ++ )
if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
{
cout << i << ' ';
j -= v[i];
}
return 0;
}
9. 有依赖的背包问题#
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int n, m;
int h[N], e[N], ne[N], idx;
int f[N][N]; //f[u, j]表示在以u为根的子树中选,总体积不超过j的最大价值
int v[N], w[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i]) //循环物品组
{
int son = e[i];
dfs(son);
//分组背包
for (int j = m - v[u]; j >= 0; j -- ) //循环体积,因为一定要选u,所以要给u留空间,从m - v[u]开始循环
for (int k = 0; k <= j; k ++ ) //循环决策(为了减少枚举次数,所以对每一棵子树按照体积分类,而不按照方案数分类)
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
//将物品u加进去
for (int i = m; i >= v[u]; i -- ) f[u][i] = f[u][i - v[u]] + w[u];
for (int i = 0; i < v[u]; i ++ ) f[u][i] = 0;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
int root;
for (int i = 1; i <= n; i ++ )
{
int p;
cin >> v[i] >> w[i] >> p;
if (p == -1) root = i;
else add(p, i);
}
dfs(root);
cout << f[root][m] << endl;
return 0;
}
2. 线性dp#
1. 数字三角形#
/*
DP
状态表示:
集合:所有从起点走到(i, j)的路径
属性:max
状态计算:
集合划分:1. 来自左上:f[i - 1][j - 1] + a[i][j]
2. 来自右上:f[i - 1][j] + a[i][j]
转移方程:f[i][j] = max (f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j])
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N], f[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
scanf("%d", &a[i][j]);
//因为求最大值,所以初始化为计算的状态为负无穷
for (int i = 0; i <= n; i ++ ) //第0行实际不存在,也要初始化为负无穷
for (int j = 0; j <= i + 1; j ++ ) //第0列和第i + 1列实际不存在,也要初始化为负无穷
f[i][j] = -INF;
f[1][1] = a[1][1]; //特殊地初始化第一个点
for (int i = 2; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);
int res = -INF;
//遍历走到最后一行的哪个点能是数字和最大
for (int i = 1; i <= n; i ++ )
res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
2. 最长上升子序列#
/*
DP
状态表示:
集合:f[i]表示所有以第i个数结尾的上升子序列
属性:最大长度
状态计算:
集合划分:第i - 1个数是 没有,a[1], a[2], ..., a[i - 1] 共i类
(每一类不一定都存在, 只有a[j] < a[i]时才存在)
转移方程:f[i] = max(f[j] + 1) (j = 0, 1, 2, ... , i - 1)
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= n; i ++ )
{
f[i] = 1; //只有i一个数,所以f[i]起码等与1
for (int j = 1; j < i; j ++ )
if (a[j] < a[i])
f[i]= max(f[i], f[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
cout << res << endl;
return 0;
}
贪心优化
- 贪心策略:因为上升子序列的结尾越小,在下一次在其结尾增添元素时,越有利于增加长度。
所以用q数组预处理出不同长度下的上升子序列的结尾的最小数 - 求最优解的过程:枚举a数组,每次找到q数组中小于a[i]的最大的数,将a[i]接到后面,即求出了一个以a[i]结尾的最长上升子序列;然后更新len为它的长度。因此枚举完a数组之后,len即为最长的上升子序列。
- q数组一定单调递增的证明: 因为q[5]所对应的是上升子序列,所以倒数第二个数肯定比倒数第一个数小,
即一定存在倒数第二个数 < q[5],那么就会存在以这个倒数第二个数为结尾的长度为4的上升子序列,就会推出q[4] < q[5],所以可以二分查找 - 每次更新时,由于q[r]是小于a[i]的最大的数,所以可以推出q[r] < a[i], q[r + 1] >= a[i], 所以当把a[i]接到后面之后,a[i]才是长度为r + 1的上升子序列的最小结尾
//优化
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
int n;
int a[N];
int q[N]; //q[k]表示所有长度为k的上升子序列的结尾的最小值
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
int len = 0; //len表示当前的最大的长度(q里面的元素个数)
q[0] = -2e9; //为了保证小于当前数的最大数一定存在,让q为哨兵,赋值为负无穷
for (int i = 0; i < n; i ++ ) //枚举每一个数
{
//二分出小于当前数的最大数的下标
int l = 0, r = len;
while (l < r)
{
int mid = (l + r + 1) >> 1;
if (q[mid] < a[i]) l = mid;
else r = mid - 1;
}
//找到r之后,要把a[i]接到r的后面,所以q的长度加1
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout << len << endl;
return 0;
}
3. 最长公共子序列#
/*
DP
状态表示:
集合:f[i, j]表示所有在a序列的前i个字母中出现,
并且在b序列的前j个字母出现的子序列
属性:Max
状态计算:
集合划分:1.不包含a[i], 不包含b[j] (f[i - 1, j - 1])
2.不包含a[i], 包含b[j] (f[i - 1, j])
3.包含a[i], 不包含b[j] (f[i, j - 1])
4.包含a[i], 包含b[j] (f[i - 1, j - 1] + 1)
转移方程:f[i, j] = max(f[i - 1, j], f[i, j - 1], f[i - 1, j - 1] + 1)
注意:第2类是f[i - 1, j]的子集,第3类是f[i, j - 1]的子集,
第1类是f[i - 1, j]和f[i, j - 1]的子集。
由于是求最大值,所以有重复不影响答案,只要都覆盖了就行
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
cin >> n >> m;
scanf("%s%s", a + 1, b + 1);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
4. 最短编辑距离#
/*DP
状态表示:
集合:f[i, j]表示所有将a[1~i]变为b[1~j]的操作方式
属性:Min
状态计算:
集合划分:按照最后一步的操作方式分为3类
1. 删 f[i - 1, j] + 1
2. 增 f[i, j - 1] + 1
3. 改 f[i - 1, j - 1] + 1或者0
转移方程:f[i, j] = min(f[i - 1, j] + 1, f[i, j - 1] + 1, f[i - 1, j - 1] + 1或者0)
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
//初始化边界情况
for (int i = 0; i <= m; i ++ ) f[0][i] = i; //a长度为0时,对a只能添加
for (int i = 0; i <= n; i ++ ) f[i][0] = i; //a长度为整个字串时,对a只能删除
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}
3. 区间dp#
1. 石子合并#
/*
状态表示:
集合:f[i, j]表示所有将第i堆石子到第j堆石子合并为一堆石子的合并方式
属性:Min
状态计算:
集合划分:以最后一次合并的分界线分类。
将区间[i, j]分为[i, k]和[k + 1, j](k从i到j - 1枚举)
转移方程:f[i, j] = min(f[i, k] + f[k + 1, j] + s[j] - s[i - 1])
(合并的最后一步必然存在,所以将最后一步的合并去掉,并不影响整体的最小值)
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> s[i];
for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];
//因为区间长度为1时只有一堆石子,合并代价为0,所以len从2开始枚举
for (int len = 2; len <= n; len ++ ) //枚举区间长度
{
for (int i = 1; i + len - 1 <= n; i ++ ) //枚举左端点
{
int l = i, r = i + len - 1;
f[l][r] = 1e8; //用到了min需要初始化为正无穷,不然永远是0
for (int k = l; k < r; k ++ ) //枚举此区间合并到最后一步时的划分线
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
cout << f[1][n] << endl;
return 0;
}
4. 计数类dp#
1. 整数划分#
/*DP
转化为完全背包问题:有一个总体积为n的背包,有n件物品,
每个物品的体积为1~n,求恰好将背包装满的方案数
状态表示:
集合:f[i, j]表示从1~i中选并且体积恰好为j的所有选法
属性:数量
状态计算:
集合划分:按照第i个物品选k个划分
转移方程:f[i, j] = f[i - 1, j] + f[i, j - v]
*/
#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];
int main()
{
cin >> n;
f[0] = 1; //什么都不选时的方案数为1
for (int i = 1; i <= n; i ++ )
for (int j = i; j <= n; j ++ )
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
5. 数位统计dp#
1. 计数问题#
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 10;
/*
001~abc-1, 999
abc
1. num[i] < x, 0
2. num[i] == x, 0~efg
3. num[i] > x, 0~999
*/
int get(vector<int> num, int l, int r)
{
int res = 0;
for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
return res;
}
int power10(int x)
{
int res = 1;
while (x -- ) res *= 10;
return res;
}
int count(int n, int x)
{
if (!n) return 0;
vector<int> num;
while (n)
{
num.push_back(n % 10);
n /= 10;
}
n = num.size();
int res = 0;
for (int i = n - 1 - !x; i >= 0; i -- )
{
if (i < n - 1)
{
res += get(num, n - 1, i + 1) * power10(i);
if (!x) res -= power10(i);
}
if (num[i] == x) res += get(num, i - 1, 0) + 1;
else if (num[i] > x) res += power10(i);
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b , a)
{
if (a > b) swap(a, b);
for (int i = 0; i <= 9; i ++ )
cout << count(b, i) - count(a - 1, i) << ' ';
cout << endl;
}
return 0;
}
6. 状态压缩dp#
1. 蒙德里安的梦想#
/*
DP
状态表示:
集合:f[i, j]表示第i列的状态为j的所有方案,
其中j是一个二进制数,含义是第i - 1列有没有支出来的横向方格
属性:数量
状态计算:
转移条件:1. 第i列与第i - 1列的状态没有冲突
2. 第i - 1列的纵向连续空白方格为偶数
转移方程:f[i, j] += f[i - 1, k](k枚举第i - 1列的所有状态)
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N;
int n, m;
LL f[N][M];
bool st[M]; //此状态是否满足纵向的连续空白方格数为偶数
int main()
{
while (cin >> n >> m, n || m)
{
memset(f, 0, sizeof f);
//预处理st数组
for (int i = 0; i < 1 << n; i ++ ) //枚举每一列的所有状态
{
st[i] = true;
int cnt = 0; //cnt表示当前状态的连续空白方格数
for (int j = 0; j < n; j ++ ) //枚举二进制数状态的每一位
if (i >> j & 1) //当前连续块结束
{
if (cnt & 1) st[i] = false;
cnt = 0;
}
else cnt ++ ;
//判断最后一次连续块
if (cnt & 1) st[i] = false;
}
//dp
f[0][0] = 1;
for (int i = 1; i <= m; i ++ ) //枚举所有列
for (int j = 0; j < 1 << n; j ++ ) //枚举第i列的所有状态
for (int k = 0; k < 1 << n; k ++ ) //枚举第i - 1列的所有状态
if (((j & k) == 0 && st[j | k]))
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
2. 最短Hamilton路径#
/*DP
状态表示:
集合:f[i, j]表示所有从0走到j,走过的所有点是i的所有路径
属性:路径长度的最小值
状态计算:
集合划分:按照倒数第二个点分类(设为k)分为n类
转移方程:f[i, j] = min(f[i, j], f[(i - (1 << j))][k] + w[k][j])
*/
#include<iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
int f[M][N];
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
cin >> w[i][j];
memset(f, 0x3f, sizeof f);
f[1][0] = 0; //从0走到0
for (int i = 0; i < 1 << n; i ++ ) //枚举所有状态
for (int j = 0; j < n; j ++ )
if (i >> j & 1) //状态i中包含第j位
for (int k = 0; k < n; k ++ ) //枚举倒数第二位是k号点
if ((i - (1 << j)) >> k & 1) //去除最后一个点后包含k号点
f[i][j] = min(f[i][j], f[(i - (1 << j))][k] + w[k][j]);
cout << f[(1 << n) - 1][n - 1];
return 0;
}
7. 树形dp#
1. 没有上司的舞会#
/*DP
状态表示:
集合:f[u, 0]表示所有从以u为根节点的子树中选择,并且不选u这个点的所有方案
f[u, 1]表示所有从以u为根节点的子树中选择,并且选u这个点的所有方案
属性:最大的快乐指数
状态计算:
(设si为u的子树的根节点)
集合划分:f[u, 0] = Σmax(f[si, 0], f[si, 1]); //因为没选u,所以子树选不选根都可以
f[u, 1] = Σmax(f[si, 0]); //因为选了u,所以子树就不能选根
转移方程:ans = max(f[u, 0], f[u, 1])
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
int w[N];
int f[N][2];
int h[N], ne[N], e[N], idx;
bool has_fa[N];
void add(int a, int b) //a -> b
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++ ;
}
//递归枚举每一个状态
void dfs(int u)
{
f[u][1] = w[u]; //选择u这个点就要加上这个点的高兴度
for (int i = h[u]; i != -1; i = ne[i]) //枚举u的所有子树的根
{
int j = e[i];
dfs(j);
f[u][1] += f[j][0];
f[u][0] += max(f[j][0], f[j][1]);
}
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> w[i];
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
cin >> a >> b;
add(b, a);
has_fa[a] = true;
}
//找到整个树的根节点的编号
int root = 1;
while (has_fa[root]) root ++ ;
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
8. 记忆化搜索#
1. 滑雪#
/*DP
状态表示:
集合:f[i, j]表示所有以(i, j)为起点开始滑的路径
属性:路径最大长度
状态计算:
集合划分:以第一步向上,向右,向下,向左分为四类
转移方程:以第一步向右为例 f[i, j] = max(f[i, j], f[i, j + 1] + 1),其他同理
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int n, m;
int h[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dp(int x, int y)
{
int &v = f[x][y]; //所有写v的地方等价于写的是f[x][y]
if (v != -1) return v; //(x, y)已经算过
v = 1; //最小也可滑自己
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y])
v = max(v, dp(a, b) + 1); //记忆化搜索
}
return v;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin >> h[i][j];
memset(f, -1, sizeof(f)); //表示(x, y)没被算过
int res = 0;
//枚举从哪个点出发
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
res = max(res, dp(i, j));
cout << res << endl;
return 0;
}
贪心
1. 区间选点#
做题技巧总结
1. 矩阵乘法#
#include <iostream>
using namespace std;
const int N = 110;
int m, n, p;
int a[N][N], b[N][N], c[N][N];
void getmatrix(int a[N][N], int m, int n)
{
for (int i = 0; i < m; i ++ )
{
for (int j = 0; j < n; j ++ )
{
scanf("%d", &a[i][j]);
}
}
}
void mul(int m, int n, int p)
{
for (int i = 0; i < m; i ++ )
for (int j = 0; j < p; j ++ )
for (int k = 0; k < n; k ++ )
{
c[i][j] += a[i][k] * b[k][j];
}
}
void putmatrix(int a[N][N], int m, int n)
{
for (int i = 0; i < m; i ++ )
{
for (int j = 0; j < n; j ++ )
{
printf("%d", a[i][j]);
if (j < n - 1) printf(" ");
}
if (i < m - 1) puts("");
}
}
int main()
{
scanf("%d%d%d", &m, &n, &p);
getmatrix(a, m, n);
getmatrix(b, n, p);
mul(m, n, p);
putmatrix(c, m, p);
return 0;
}
2. 有序数组中二分查找某数的范围#
3. 进位制转化#
-
十进制数转化为b进制数(短除法)
短除法的正确性证明
char get(int x) //将数字转化为字符 { if (x <= 9) return x + '0'; return x - 10 + 'A'; } string base(int num, int b) //将一个十进制数num转化为b进制数 { string res; while (num) { res += get(num % b); num /= b; } reverse(res.begin(), res.end()); return res; }
- AcWing 1346. 回文平方
-
b进制数转为为十进制数(秦九韶算法)
int get(char c) //将字符转化为数字 { if (c <= '9') return c - '0'; return c - 'A' + 10; } int base(string num, int b) //将一个b进制数num转化为十进制数 { int res = 0; for (auto c : num) { res = res * b + get(c); } return res; }
-
a进制数转化为b进制数(短除法)
reverse(num.begin(), num.end()); vector<int> res; while (num.size()) { int r = 0; for (int i = num.size() - 1; i >= 0; i -- ) { num[i] += r * a; r = num[i] % b; num[i] /= b; } res.push_back(r); //去除前导0 while (num.size() && num.back() == 0) num.pop_back(); } reverse(res.begin(), res.end());
4. 有关回文#
-
判断回文串
bool check(string s) { for (int i = 0, j = s.size() - 1; i < j; i ++, j -- ) { if (s[i] != s[j]) return false; } return true; }
-
判断回文数
bool check(int num) { int t = num, res = 0; while (t != 0) { res = res * 10 + t % 10; t /= 10; } if (res == num) return true; else return false; }
5. 去重#
6. 日期问题#
-
判断闰年
bool is_leap(int y) { if ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) return true; else return false; }
7. 求字符串中ASSIC最大的字符的坐标#
int get(string s)
{
int p = 0;
for (int i = 1; i < s.size(); i ++ )
{
if (s[i] > s[p]) p = i;
}
return p;
}
8. 取整问题转化#
- n/s(上取整)等价于 (n + s - 1) / s(下取整)
9. 筛素数#
#include<stdio.h>
#define MAX_SIZE 500001
void get_prime_table(int is_not_prime[])
{
int num, i;
is_not_prime[0] = is_not_prime[1] = 1;
is_not_prime[2] = 0;
for (num = 2; num <= MAX_SIZE / num; num ++)
{
if(is_not_prime[num] == 0) //没被筛过
for (i = num * 2; i < MAX_SIZE; i += num) is_not_prime[i] = 1;
}
}
int main()
{
int i, begin, end, flag = 1, flag1 = 0;
int is_not_prime[MAX_SIZE] = {0}; // 1 -> 不是素数
get_prime_table(is_not_prime);
while (scanf("%d %d", &begin, &end)!=EOF)
{
if (flag1) printf("\n");
for (i = begin; i <= end; i ++)
{
if (is_not_prime[i] == 0)
{
printf("%d\n", i);
flag = 0;
}
}
if (flag) printf("\n");
flag = 1;
flag1 = 1;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现