复习资料
排序算法
选择排序O(n^2)(不稳定)
for(i=1;i<=n;i++){
k = i;
for(j=i+1;j<=n;j++)
if(a[k]>a[j]) k = j;
if(k!=i)
swap(a[i],a[k]);
}
插入排序O(n^2)(稳定)
for (i = 1; i <= n; i++) {
cin >> a[i];
for (j = i - 1; j > 0; j--)
if (a[j] < a[i]) break;
if (j != i - 1) {
t = a[i];
for (k = i - 1; k > j; k--)
a[k + 1] = a[k];
a[k + 1] = t;
}
}
冒泡排序O(n^2)(稳定)
for(i=n-1;i>=1;i--){
ok = true;
for(j=1;j<=i;j++)
if(a[j]>a[j+1]){
swap(a[j],a[j+1]);
ok = false;
}
if(ok) break;
}
桶排序O(n)(稳定)
for(i=1;i<=n;i++){
cin>>x;
b[x]++;
}
for(i=1;i<=MAXN;i++)
while(b[i]--)
cout<<i<<" ";
快速排序O(nlogn)(不稳定)
void qsort(int l, int r) {
int mid, i = l, j = r;
mid = a[(l + r) / 2];
while (i <= j) {
while (a[i] < mid) i++;
while (a[j] > mid) j--;
if (i <= j)
swap(a[i++], a[j--]);
}
if (l < j) qsort(l, j);
if (r > i) qsort(i, r);
}
归并排序O(nlogn)(稳定)
void msort(int l, int r) {
if (l == r) return; //分解至一个数字为止
int mid = (l + r) / 2;
msort(l, mid);
msort(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) { //合并左右序列
if (a[i] <= a[j]) temp[k++] = a[i++];
else temp[k++] = a[j++];
}
while (i <= mid) temp[k++] = a[i++];
while (j <= r) temp[k++] = a[j++]; //复制左右序列剩余
for (int i = l; i <= r; i++) a[i] = temp[i];
}
堆排序O(nlogn)(不稳定)
int a[100], len = 0;
void put(int x) {
int now, next;
now = ++len;
a[now] = x;
while (now > 1) {
next = now >> 1;
if (a[next] < a[now]) return;
swap(a[next], a[now]);
now = next;
}
return;
}
void del() {
int now, next;
a[1] = a[len--];
now = 1;
while (now * 2 <= len) {
next = now << 1;
if (next < len && a[next] > a[next + 1]) next++;
if (a[now] <= a[next]) return;
swap(a[next], a[now]);
now = next;
}
return;
}
int get() {
int d = a[1];
del();
return d;
}
int main() {
int x, n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> x;
put(x);
}
for (int i = 1; i <= n; i++) {
cout << get() << " ";
}
return 0;
}
基数排序O(n)(稳定)
一种非比较算法,其原理是将整数按每个位数分别比较。它利用了桶的思想。
树形选择排序(锦标赛排序)O(nlogn)(稳定)
一种按照锦标赛的思想进行选择的排序方法,该方法是在简单选择排序方法上的改进。简单选择排序,花费的时间大部分都浪费在值的比较上面,而锦标赛排序刚好用树保存了前面比较的结果,下一次比较时直接利用前面比较的结果,这样就大大减少比较的时间,从而降低了时间复杂度,由 \(O(n^2)\) 降到 \(O(nlogn)\) ,但是浪费了比较多的空间,“最大的值”也比较了多次。为了弥补这些缺点,1964年,堆排序诞生。
数学相关
n进制数转十进制数
scanf("%d\n", &n); //输入 n进制数
gets(s); //输入数字
int len = strlen(s); //获取长度
if (s[0] >= 'A') ans = s[0] - 55;
else ans = s[0] - '0';
for (int i = 1; i < len; i++) {
if (s[i] >= 'A') ans = ans * n + s[i] - 55;
else ans = ans * n + s[i] - '0'; //秦九韶算法
}
printf("%d", ans);
栈模拟将十进制数字n 转化为d 进制数字
//a[16]存1-F
while (n) { //char data[100],int top
s.data[s.top++] = a[n % d];
n /= d; }
while (s.top)
cout << s.data[--s.top];
欧几里得算法(辗转相除法)
int gcd(int a, int b) { //求最大公因数
int temp;
while (b) {
temp = a % b;
a = b;
b = temp;
}
return a;
// return b ? gcd(b, a%b) : a; // 方法二
// return __gcd(a, b); // 方法三:#include <algorithm>
}
最小公倍数
x * y / gcd(x,y);
区间和(离散化)
int n, m;
int a[N], s[N];
vector<int> xb; //存用到的所有坐标
vector<PII> add, q;
int main() {
cin >> n >> m;
for (int i = 0; i < n; i ++ ) {
int x, c;
cin >> x >> c; //在下标为x的位置插值c
add.push_back({x, c});
xb.push_back(x);
}
for (int i = 0; i < m; i ++ ) {
int l, r;
cin >> l >> r;
q.push_back({l, r});
xb.push_back(l);
xb.push_back(r);
}
// 去重
sort(xb.begin(), xb.end());
xb.erase(unique(xb.begin(), xb.end()), xb.end());
// 处理插入
for (auto i : add) {
int x = lower_bound(xb.begin(), xb.end(), i.first) - xb.begin() + 1; //映射下标,+1为使下标从1开始
a[x] += i.second;
}
// 预处理前缀和
for (unsigned int i = 1; i <= xb.size(); i ++ )
s[i] = s[i - 1] + a[i];
// 处理询问
for (auto i : q) {
int l = lower_bound(xb.begin(), xb.end(), i.first) - xb.begin() + 1;
int r = lower_bound(xb.begin(), xb.end(), i.second) - xb.begin() + 1;
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
差分
构造b数组使得a数组是b数组的前缀和。简单来说,就是求前缀和的逆运算
用途:将a数组的[l,r]区间内的数都分别加常数c
一维差分
void insert(int l, int r, int x) { //将a数组的[l,r]区间内的数都分别加常数c
b[l] += x;
b[r + 1] -= x;
}
//求b数组的前缀和,也就是修改后的a数组了
二维差分
void insert(int x1, int y1, int x2, int y2, int x) {
a[x1][y1] += x;
a[x2 + 1][y1] -= x;
a[x1][y2 + 1] -= x;
a[x2 + 1][y2 + 1] += x;
}
二维数组前缀和
for (i = 1; i <= n; i++)
for (j = 1; j <= m; j++)
a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
高精度算法
高精度加法
string s1, s2;
int a[N], b[N], c[N], lena, lenb, lenc;
int i, x;
cin >> s1 >> s2;
lena = s1.size();
lenb = s2.size();
for (i = 0; i < lena; i++)
a[lena - i] = s1[i] - '0'; //一个加数放入a数组
for (i = 0; i < lenb; i++)
b[lenb - i] = s2[i] - '0'; //一个加数放入b数组
lenc = 1, x = 0;
while (lenc <= lena || lenc <= lenb) { //核心:两数相加
c[lenc] = a[lenc] + b[lenc] + x;
x = c[lenc] / 10; //x为进位
c[lenc] %= 10;
lenc++;
}
c[lenc] = x;
if (c[lenc] == 0) lenc--; //处理最高位
for (i = lenc; i > 0; i--)
cout << c[i];
高精度减法
if (s1 == s2) {
cout << 0;
return 0;
}
if (s1.size() < s2.size() || (s1.size() == s2.size() && s1 < s2)) {
swap(s1, s2);
cout << "-";
}
lena = s1.size(), lenb = s2.size();
for (i = 0; i < lena; i++)
a[lena - i] = s1[i] - '0';
for (i = 0; i < lenb; i++)
b[lenb - i] = s2[i] - '0';
i = 1;
while (i <= lena || i <= lenb) {
if (a[i] < b[i]) {
a[i] += 10; //借1当10
a[i + 1]--;
}
c[i] = a[i] - b[i]; //对应位减
i++;
}
lenc = i;
while (c[lenc] == 0 && lenc > 1) lenc--; //去除前导0
高精度乘法
for (i = 1; i <= lena; i++) {
x = 0; //存放进位
for (j = 1; j <= lenb; j++) { //对乘数每一位处理
c[i + j - 1] = a[i] * b[j] + x + c[i + j - 1]; //当前乘积+进位+原数
x = c[i + j - 1] / 10;
c[i + j - 1] %= 10;
}
c[i + lenb] = x; //进位
}
lenc = lena + lenb;
while (c[lenc] == 0 && lenc > 1) lenc--;
高精除以低精
lena = s.size();
for (i = 0; i < lena; i++)
a[i + 1] = s[i] - '0';
for (i = 1; i <= lena; i++) { //按位相除
c[i] = (x * 10 + a[i]) / b;
x = (x * 10 + a[i]) % b;
}
lenc = 1;
while (c[lenc] == 0 && lenc < lena) lenc++;
for (i = lenc; i <= lena; i++)
cout << c[i];
cout << endl << x; //输出余数
质数筛
朴素筛法(埃氏筛法)
缺点:一个数会被它所有的因数都删一次,存在重复计算。
bool s[N]; //不是素数为真,是素数为假
void prime() {
int i, j;
s[0] = s[1] = true; //不是素数
for (i = 2; i <= N; i++) {
if (s[i] == 0) {
for (j = i + i; j <= N; j += i) //删去素数的倍数
s[j] = true;
}
}
}
线性筛(欧拉筛法)
int p[N], t = 0;
bool s[N]; //0表示是质数
void prime(int n) {
int i, j;
s[0] = s[1] = 1; //不是素数
for (i = 2; i <= n; i++) {
if (!s[i]) p[t++] = i; //下一行用除法是防止 爆int
for (j = 0; p[j] <= n / i; j++) { //一个数会被它最小的因数删掉,线性筛
s[i * p[j]] = 1;
if (i * p[j] == 0) break;
}
}
}
二分法
二分查找
1.找到第一个大于num的数字的下标
#include <algorithm>
int search1(int arr[],int n,int num){
return lower_bound(arr + 1,arr + 1 + n,num) - arr;
}
int search1(int arr[], int n, int num) {
int l = 1, r = n, res = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (arr[mid] > num) {
res = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return res;
}
2.找到第一个大于等于num的数字的下标
#include <algorithm>
int search2(int arr[],int n,int num){
return upper_bound(arr + 1,arr + 1 + n,num) - arr;
}
int search2(int arr[], int n, int num) {
int l = 1, r = n, res = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (arr[mid] >= num) {
res = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return res;
}
二分答案(浮点型)
const double eps = 1e-8;//精度控制范围
double gen(double x) { //求三次方根
double r, l;
if (x == 0) return 0;
if (x > 0) { //利用x^3函数性质确定范围
l = 0;
r = x <= 1 ? 1 : x;
}
if (x < 0) {
r = 0;
l = x >= -1 ? -1 : x;
}
while (r - l > eps) { //二分核心
double mid = (r + l) / 2;
if (mid * mid * mid >= x)
r = mid;
else
l = mid;
}
return l;
}
dfs-数独
void dfs(int x, int y) {
if (x == 9) //搜出界,输出
return;
if (!(a[x][y])) {
for (int k = 1; k <= N; k++)
if (!(b[x][k] || c[y][k] || d[x / 3][y / 3][k])) {
a[x][y] = k; //int棋盘
b[x][k] = 1; //bool行
c[y][k] = 1; //bool列
d[x / 3][y / 3][k] = 1;//bool宫
dfs(x + (y + 1) / N, (y + 1) % N);
a[x][y] = 0; //回溯
b[x][k] = 0;
c[y][k] = 0;
d[x / 3][y / 3][k] = 0;
}
}
else
dfs(x + (y + 1) / N, (y + 1) % N);
}
bfs-走迷宫
int sx, sy, fx, fy, n, m;//起点,终点,n行m列
int a[100][100]; //地图
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0}; //R,D,L,U
struct node {
int x, y, step;
};
void bfs(int x, int y, int step) {
queue<node> p; //定义队列 p
p.push({x, y, step});
a[x][y] = 1; //起点标记为走过
while (!p.empty()) {
node t = p.front(); //替换队首
p.pop(); //删去队首
for (int i = 0; i < 4; i++) {
int xx = t.x + dx[i], yy = t.y + dy[i];
if (a[xx][yy] == 0 && xx > 0 && xx <= n && yy > 0 && yy <= m) {
a[xx][yy] = 1; //标记为已走过
p.push({xx, yy, t.step + 1}); //满足条件即加入队列
if (xx == fx && yy == fy) {
cout << t.step + 1 << endl; //到达终点即输出
return;
}
}
}
}
}
int main() {
cin >> n >> m >> sx >> sy >> fx >> fy;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
bfs(sx, sy, 0);
return 0;
}
数学库函数 <cmath>
向上取整 ceil(double x)
自然指数 exp(double x)
自然对数 log(double x)
常用对数 log10(double x)
以2为底的对数 log2(double x)
四舍五入取整 round(double x)
x的n次幂 pow(double x,double n)
标准算法库函数 <algorithm>
翻转函数:reverse(a+1,a+1+n);
翻转x-y区间的数组、容器的值;
排序函数:sort(a+1,a+1+n,cmp);
查找函数:find(a,a+n,3);
查找某数组指定区间x-y内是否有x,若有,则返回该位置的地址,若没有,则返回该数组第n+1个值的地址。
upper_bound(a+1,a+1+n):
查找第一个大于x的值的地址
lower_bound(a+1,a+1+n):
查找第一个大于等于x的值的地址
填充函数:fill(a+3,a+5,0x3f3f3f3f)
在区间内填充某一个值。同样适用所有类型数组,容器。
查找某值出现的次数:count(a+1,a+1+n,88)
求最大公因数:__gcd(a,b) 注意两个下划线
求交集:
vector<int> v;
set_intersection(a,a+5,b,b+5,inserter(v,v.begin()));
将两个数组的交集赋给一个容器(为什么不能赋给数组呢?因为数组不能动态开辟,且inserter()函数中的参数必须是指向容器的迭代器。)
求并集:
set_union(a,a+5,b,b+5,inserter(v,v.begin()));
求差集:set_difference()
全排列:next_permutation(a,a+n)
将给定区间的数组、容器的下一个全排列
并查集
for (i = 1; i <= n; i++)
fa[i] = i; //初始化:每个点就是一个集合
int myfind(int x) { //找到元素所在集合名字
if (fa[x] != x)
fa[x] = myfind(fa[x]); //路径压缩
return fa[x];
}
void myunion(int x, int y) { //合并两个元素所在集合
int r1 = myfind(x);
int r2 = myfind(y);
if (r1 != r2)
fa[r1] = r2;
}
bool check(int x, int y) { //判断两个元素是否在同一个集合
int r1 = myfind(x);
int r2 = myfind(y);
return r1 == r2;
}
动态规划
01背包
有N件物品和容量为V的背包。每种物品仅一件,可选择放或不放。
\(f[i][v]\): 前i件物品总重量不超过v的最优价值
f[i][v] = max{f[i - 1][v], f[i - 1][v - w[i]] + c[i]};
完全背包
有N件物品和容量为V的背包。每种物品都有无限件可放。
\(f[i][v]\): 前i件物品总重量不超过v的最优价值。
f[i][v] = max{ f[i - 1][v], f[i][v - w[i]] + c[i]};
优化:二进制思想,转化为01背包求解。
把第i种物品拆成费用为 \(w[i] * 2^k\) ,价值为 \(c[i] * 2^k\) 的若干件物品,其中 \(k\) 满足 \(w[i] * 2^k<V.\) (不管最优策略选几件第 \(i\) 种物品,总可以表示为若干个 \(2^k\) 件物品的和)
多重背包
有N件物品和容量为V的背包。第i件物品最多有n[i]件可用。
f[i][v] = max{f[i - 1][v - k * w[i] + k * c[i]| 0<=k<=n[i]}; O(V * Σn[i])
优化:二进制思想,转化为01背包求解。
for (i = 1; i <= n; i++) {
t = 1;
scanf("%d%d%d", &x, &y, &s); //重量,价值,个数
while (s >= t) {
v[++n1] = x * t;
w[n1] = y * t;
s -= t;
t *= 2;
}
v[++n1] = x * s;
w[n1] = y * s; //把s以2的指数分堆:1,2,4,...,2^(k-1),s-2^k+1;
}
二维费用的背包问题
\(f[i][v][u]\) :表示前i件物品付出两种代价分别为v和u时可获得的最大价值。
f[i][v][u] = max{f[i - 1][v][u], f[i - 1][v - w1[i]][u - w2[i]] + c[i]};
最长不下降子序列长度
\(f[i]\): 表示长度为i的最长不下降序列的末端最优值。
int len = 1;
f[len] = x;
for (int i = 1; i < n; i++) {
cin >> x;
if (x >= f[len]) f[++len] = x;
else {
//二分法:找到f[]中第一个>x的数字的下标
int s = upper_bound(f + 1, f + len + 1, x) - f;
f[s] = x;
}
}
printf("%d\n", len); //仅输出长度而无法输出路径
字符串DP-编辑距离
通过三种操作:删除、插入、修改将字符串A转换成B的最少操作次数。
\(f[i][j]:a\) 的前 \(i\) 项改到 \(b\) 的前 \(j\) 项的最少操作。
/*
(假设首个字符是a[1])
f[i - 1][j - 1] + 1 把a[i]改为b[j]
f[i][j - 1] + 1 在a[i]后插入b[j-1]
f[i - 1][j] + 1 删去a[i]
*/
for (i = 1; i <= len1; i++)
f[i][0] = i; //a的前i项全部删除
for (i = 1; i <= len2; i++)
f[0][i] = i; //在开头给字符串a加上和b前i项一样的字符
for (i = 1; i <= len1; i++)
for (j = 1; j <= len2; j++) {
if (a[i - 1] == b[j - 1]) f[i][j] = f[i - 1][j - 1]; //两个字符相同
else f[i][j] = min(min(f[i - 1][j - 1], f[i][j - 1]), f[i - 1][j]) + 1;
}
cout << f[len1][len2] << endl;
RMQ算法
即区间最值查询。RMQ算法一般用较长时间做预处理,时间复杂度为 \(O(nlogn)\) ,然后可以在 \(O(1)\) 的时间内处理每次查询。
二维数组dp[i][j]表示从第i位开始连续 \(2^j\) 个数中的最小值。求 \(dp[i][j]\) 的时候可以把它分成两部分,第一部分是从 \(i\) 到 \(i+2^{j-1}-1\) ,第二部分从 \(i+2^{j-1}\) 到 \(i+2^j-1\) 。
for (i = 1; i <= N; i++)
dp[i][0] = a[i]; //初始化
for (int j = 1; (1 << j) <= N; j++) //注意循环顺序
for (int i = 1; i + (1 << j) - 1 <= N; i++)
dp[i][j] = min(dp[i][j - 1], dp[i + (1 << j - 1)][j - 1]);
RMQ的查询部分
假设我们需要查询区间 \([l ,r]\) 中的最小值,令 \(k = log_2(r-l+1)\) , 则区间 \([l, r]\) 的最小值 RMQ[l,r] = min(dp[l][k], dp[r - (1 << k)+ 1][k]);
dp[l][k] 维护的是区间[l, l + 2^k - 1], dp[r - (1 << k) + 1][k]维护的是区间 [r - 2^k + 1, r] .且 r-2^k+1 ≤ l+2^k-1 。
树形DP-没有上司的舞会
void dp(int fa) {
//f[fa][0] = sum( max(f[son][0],f[son][1]) );
上司参加舞会的最大快乐值
//f[fa][1] = sum(f[son][0]) + r[fa];
上司不参加舞会的最大快乐值
if (tree[fa].empty()) {
f[fa][0] = 0;
f[fa][1] = r[fa];
return;
}
for (unsigned int i = 0; i < tree[fa].size(); i++) {
int son = tree[fa][i];
dp(son);
f[fa][0] += max(f[son][0], f[son][1]);
f[fa][1] += f[son][0];
}
f[fa][1] += r[fa];
}
dp(rt);
cout << max(f[rt][0], f[rt][1]) << endl;
树
建立二叉树
void build(int rt) {
if (i >= s.length()) return;
a[rt] = s[i];
if (s[i] == '.') return;
i++;
build(rt << 1); //左节点利用二叉树的性质建树
i++;
build(rt << 1| 1); //右节点
}
二叉树求先序排列
string h, z; //后序,中序
cin >> z >> h;
calc(0, h.length() - 1, 0, z.length() - 1);
cout << endl;
void calc(int l1, int r1, int l2, int r2) {
int m = z.find(h[r1]);
cout << h[r1];
if (m > l2) calc(l1, r1 - r2 + m - 1, l2, m - 1);
if (m < r2) calc(r1 - r2 + m, r1 - 1, m + 1, r2);
}
二叉树知先序求中/后序排列
void dfsh() { //求后序排列
char root;
root = s[t];
if (root != '.') {
t++;
dfsh();
t++;
dfsh();
cout << root;
}
}
邻接表建多叉树
vector<int> tree[1010]; //每个vector数组都是 一个子树 空间复杂度O(n)
for (i = 1; i <= n - 1; i++) { //n个顶点的树 一共有n-1条边
cin >> x >> y; //输入应从根节点开始
tree[x].push_back(y); //x的子节点是y,入度为0的节点是根节点
}
深搜遍历二叉树,dp计算深度
void dfs_deep(int rt, int fa) { //遍历树
deep[rt] = deep[fa] + 1; //当前节点深度 等于 父节点深度+1
//用i枚举编号为rt的节点的所有子节点
for (unsigned int i = 0; i < tree[rt].size(); i++) { //注意是 <
int to = tree[rt][i];
if (to == fa) continue;
dfs_deep(to, rt);
}
}
任意树最近公共祖先(LCA)暴力爬树
Lowest Common Ancestors
int LCA(int a, int b) {
if (deep[a] < deep[b])
swap(a, b);
while (deep[a] > deep[b])
//让深度大的节点与另一个节点等深
a = fa[a];
while (a != b) {
//同时往上找,直到找到公共祖先
a = fa[a];
b = fa[b];
}
return a;
}
任意树最近公共祖先(LCA)倍增法
int f[N][18]; //数组f[x][k]表示x往上跳2^k步所能够到达的点,k∈[0,floor(log2(n))] 建议算好直接写数字
f[rt][0] = fat; //以下写在dfs里
for (int k = 1; k <= 15; k++) {
int t = f[rt][k - 1]; //先跳2^(k-1)
f[rt][k] = f[t][k - 1]; //再跳2^(k-1)
}
int LCA(int a, int b) {
if (deep[a] < deep[b])
swap(a, b);
// 把a跳到和b同一层
for (int i = 15; i >= 0; i --) {
if (deep[f[a][i]] >= deep[b]) {
a = f[a][i];
}
}
if (a == b) return a;
//下面是a和b同时往上跳,此时a和b是在同一层的
for (int i = 15; i >= 0; i --) {
if (f[a][i] != f[b][i]) {
a = f[a][i], b = f[b][i];
}
}
return f[a][0];
}
线段树
区间最大值
struct Node {
int l, r; //区间左右端点必须存
int ma; //区间[l,r]的最大值
} tr[4 * N];
void pushup(int rt) { //由两个子节点信息计算父节点信息
tr[rt].ma = max(tr[rt << 1].ma, tr[rt << 1 | 1].ma);
}
void build(int rt, int l, int r) {
tr[rt] = {l, r};
if (l == r){
tr[rt].ma = w[l];
return;
}
int mid = (l + r) >> 1;
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
pushup(rt);
}
int query(int rt, int l, int r) {
if (tr[rt].l >= l && tr[rt].r <= r) //树中节点已被完全包含于[l,r]中
return tr[rt].ma;
int mid = (tr[rt].l + tr[rt].r) >> 1;
int v = 0;
if (l <= mid) v = query(rt << 1, l, r);
if (r > mid) v = max(v, query(rt << 1 | 1, l, r));
return v;
}
void modify(int rt, int x, int v) { //将下标为x的点修改为v
if (tr[rt].l == x && tr[rt].r == x)
tr[rt].ma = v;
else {
int mid = (tr[rt].l + tr[rt].r) >> 1;
if (x <= mid) modify(rt << 1, x, v);
else modify(rt << 1 | 1, x, v);
pushup(rt); //回溯
}
}
区间和
struct Node {
int l, r;
LL sum; //区间和,仅考虑当前节点及子节点上的所有标记,不考虑父节点上的懒标记
LL add; //add为懒标记:给当前节点的所有儿子加上add
} tr[N * 4];
void pushup(int rt) { //向上更新
tr[rt].sum = tr[rt << 1].sum + tr[rt << 1 | 1].sum;
}
void pushdown(int rt) { //向下分裂
Node &rrt = tr[rt], &ll = tr[rt << 1], &rr = tr[rt << 1 | 1];
if (rrt.add) {
ll.add += rrt.add;
ll.sum += (LL)(ll.r - ll.l + 1) * rrt.add;
rr.add += rrt.add;
rr.sum += (LL)(rr.r - rr.l + 1) * rrt.add;
rrt.add = 0;
}
}
void build(int rt, int l, int r) {
if (l == r) {
tr[rt] = {l, r, w[l], 0};
return;
}
tr[rt] = {l, r, 0, 0};
int mid = (l + r) >> 1;
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
pushup(rt);
}
void modify(int rt, int l, int r, int v) {
if (tr[rt].l >= l && tr[rt].r <= r) {
tr[rt].add += v;
tr[rt].sum += (LL)(tr[rt].r - tr[rt].l + 1) * v;
} else {
pushdown(rt); //先分裂
int mid = (tr[rt].l + tr[rt].r) >> 1;
if (l <= mid) modify(rt << 1, l, r, v);
if (r > mid) modify(rt << 1 | 1, l, r, v);
pushup(rt);
}
}
LL query(int rt, int l, int r) {
if (tr[rt].l >= l && tr[rt].r <= r) return tr[rt].sum;
pushdown(rt); //先分裂
int mid = (tr[rt].l + tr[rt].r) >> 1;
LL res = 0;
if (l <= mid) res = query(rt << 1, l, r);
if (r > mid) res += query(rt << 1 | 1, l, r);
return res;
}
图论算法
图的存储:邻接表前向星(链式前向星)
//图的储存:邻接表前向星(链式前向星)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e6 + 10; //顶点数
const int M = 1e6 + 10; //边数
int n, m;
struct E {
int to, w, next;
} e[M]; //存储边的信息
int tot, head[N]; //head[i]表示顶点i出发的第一条边编号
void addEdge(int u, int v, int w) { //边u->v,权重w
e[++tot] = {v, w, head[u]};
head[u] = tot;
}
void output() {
int i, j;
for (i = 1; i <= n; i++) {
for (j = head[i]; j; j = e[j].next) { //遍历
int v = e[j].to;
int w = e[j].w;
printf("%d->%d w=%d\n", i, v, w);
}
}
}
int main() {
int n, m;
cin >> n; //点的个数
cin >> m; //边的个数
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
addEdge(u, v, w);
}
output();
return 0;
}
最短路径-Floyed
//O(n^3),适用于:多源,负边权,但不可判断负权回路
void Floyed() {
int k, i, j;
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if ((i != j) && (i != k) && (j != k))
if (mp[i][j] > mp[i][k] + mp[k][j]){
mp[i][j] = mp[i][k] + mp[k][j]; //更新最短路径
pre[i][j] = pre[k][j]; //更新j的前驱
}
}
最短路径-Dijkstra(链式前向星)
//O(n^2),适用于:单源,正边权,不可判断负权回路。最近蓝点设白点,更新最短距。
void Dijkstra(int s) { //求源点s到其他各点的最短路径
memset(vis, 0, sizeof(vis));
//0:蓝点(未确定最短路径的点),1:白点(已确定最短路径的点)
memset(dis, 0x3f, sizeof(dis));
//dis[i]:s到点i的最短距离,初始无穷大
dis[s] = 0;
pre[s] = 0;
int i;
while(1) { //遍历每个点
int blue = -1, minx = INF;
for (i = 1; i <= n; i++) {
if (!vis[i] && minx > dis[i]) { //从蓝点中找出最小的点
blue = i; //更新最短距离的点,即找到需变为白点的蓝点
minx = dis[i]; //更新最短距离
}
}
//如果没有蓝点,结束计算
if (blue == -1) break;
vis[blue] = true; //将该蓝点设置为白点
for (i = head[blue]; i != -1; i = e[i].next) {
int temp = e[i].to;
if (!vis[temp])
if(dis[temp]>dis[blue] + e[i].w){
dis[temp] = dis[blue] + e[i].w;
pre[temp] = blue;
}
}
}
}
最短路径-Dijkstra(堆优化+链式前向星)
//O((m+n)logn),适用于:单源,正边权,不可判断负权回路
#define pa pair<int, int>
priority_queue<pa, vector<pa>, greater<pa> > q; //小根堆
void Dijkstra(int s) { //求源点s到其他各点的最短路径
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis)); dis[s] = 0;
pre[s] =0;
q.push(make_pair(0, s)); //先比较pair的第一项,第一项相同再比较第二个,因此dis写前面
int i;
while (!q.empty()) {
pa temp = q.top(); //从蓝点中找出最小的点
q.pop();
int blue = temp.second; //更新最短距离的点,即找到需变为白点的蓝点
if (vis[blue]) //弹出队头并不会将全部编号相同的点弹出,因此队列中可能有白点
continue;
vis[blue] = true; //将该蓝点设置为白点
for (i = head[blue]; i; i = e[i].next) {
int temp = e[i].to;
if (!vis[temp])
if(dis[temp]>dis[blue]+e[i].w){
dis[temp]=dis[blue]+e[i].w;
pre[temp] = blue;
q.push({dis[temp],temp});
}
}
}
}
最短路径-Bellman-Ford(邻接表数组模拟)
//O(nm),适用于:单源,负边权,可判断负权回路。枚举m条边松弛n-1次。
bool Bellman_Ford(int s) {
int i, j;
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
pre[s] = 0;
for (i = 1; i <= n - 1; i++) { //最多松弛n-1次
bool check = false;
//枚举每一条边
for (j = 1; j <= m; j++) {
if (dis[v[j]] > dis[u[j]] + w[j]) { //进行松弛操作
dis[v[j]] = dis[u[j]] + w[j];
// pre[v[j]] = u[j];
check = true;
}
}
if (!check) break; //防止重复运算
}
//判断是否有负权回路
bool flag = false;
for (i = 1; i <= m; i++) {
if (dis[v[i]] > dis[u[i]] + w[i]) { 还能进行松弛操作
flag = true;
break;
}
}
return flag;
}
最短路径-SPFA(链式前向星)
Sortest Path Faster Algorithm.O(km),适用于:单源,负边权,可判断负权回路。
void SPFA(int s) {
queue<int> q;
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
pre[s] = 0;
q.push(s);
vis[s] = true; //s在队列中
while (!q.empty()) {
int f = q.front();
q.pop();
vis[f] = false;
for (int i = head[f]; i != -1; i = e[i].next) {
int v = e[i].to;
int w = e[i].w;
if (dis[v] > dis[f] + w) { //能松弛就松弛
dis[v] = dis[f] + w;
pre[v] = f;
if (!vis[v]) {
q.push(v);
vis[v] = true;
}
}
}
}
最短路径输出
cout<<s;
void print(int x) {
if(pre[x] == 0)
return;
print(pre[x]);
printf("->");
write(x);
}
最小生成树(MST)-Kruskal
O(nlogn),n为边数,利用并查集实现
sort(e + 1, e + 1 + tot, cmp); //先按边权由小到大排序
int k = 0, mst = 0;
for (i = 1; i <= tot; i++) { //按顺序遍历边
if (myfind(e[i].u) != myfind(e[i].v)) {
myunion(e[i].u, e[i].v);
k++;
mst += e[i].w;
}
if (k == n - 1) break;
}
cout << mst << endl;
最小生成树(MST)-Prim
O(nlogn),采用与Belman-Ford算法相似的白蓝点思想,进行了堆优化
dis表示蓝点i与树的最短距离(最小边权)
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[1] = 0; //初始化,因为从1开始遍历
q.push({0, 1});
while (!q.empty() && cnt<n) {
pa temp = q.top();
q.pop();
int blue = temp.second;
if (vis[blue]) continue;
vis[blue] = true;
cnt++; //常数级优化
mst += dis[blue];
for (i = head[blue]; i; i = e[i].next) {
int to = e[i].to;
if (!vis[to])
if (dis[to] > e[i].w) {
dis[to] = e[i].w;
q.push({dis[to], to});
}
}
}
//输出MST
cout << mst << endl;
Tarjan算法
割点:无向连通图中,某点和连接点的边去掉后,图不再连通
割边(桥): 无向连通图中,某条边去掉后,图不再连通
桥与割点的关系:
1.有割点不一定存在桥,有桥一定存在割点。
2.桥的两个端点至少有一个是割点。
图的割边
int dfn[N], low[N]; //dfn:时间戳;low:追溯值
//dfn[x]:用来标记图中每个节点在进行dfs时被访问的时间先后顺序;
//low[x]:dfs中,x通过回边(非父子边)能绕到的最早的点(即dfn最小)的dfn值;
int tim = 0; //时间戳
void Tarjan(int u, int f) { //f为u的父亲
dfn[u] = low[u] = ++tim;
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].to;
if (!dfn[v]) { //该点还未被访问过
Tarjan(v, u); //dfs过程
low[u] = min(low[u], low[v]);
//回溯时更新u的追溯值
if(low[v]>dfn[u])
//相比割点问题,桥只有一个条件,函数也就没必要有第三个参数root
printf("%d->%d是桥\n",u,v);
} else if (v != f) {
//说明下一个点是祖先而不是父亲
low[u] = min(low[u], dfn[v]);
//更新追溯值
}
}
}
非连通图写法
for(i=1;i<=n;i++){
if(!dfn[i]) //说明没访问过
Tarjan(i,-1); }
求强连通分量模板
int color[N], all[N],du[N];
//color存i点的颜色,all存i颜色的个数,即强连通分量的大小
void Tarjan(int u) {
dfn[u] = low[u] = ++tim;
s.push(u);
vis[u] = true;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (!dfn[v]) { //该点还未被访问过
Tarjan(v); //dfs过程
low[u] = min(low[u], low[v]); //回溯时更新u的追溯值
} else if (vis[v]) { //说明下一个点是祖先而不是父亲
low[u] = min(low[u], dfn[v]); //更新追溯值
}
}
int hd;
//存栈顶元素 //说明了u点及u点之下的所有子节点没有边是指向u的祖先的了,
if (low[u] == dfn[u]) {
//即u点与它的子孙节点构成了一个最大的强连通图即强连通分量
clr++;
do {
hd = s.top();
s.pop();
vis[hd] = false;
color[hd] = clr;
all[clr]++;
//将一个分量中的元素染成一色
} while (u != hd);
}
}
for(i=1;i<=n;i++){
for(int j = head[i];j;j=e[j].next){
int v = e[j].to;
if(color[i]!=color[v])
//颜色相同的看作一个点 缩点
du[color[i]]++;
//记录每个点的出度
}
}
割点
bool cut[N]; //判断是否为割点
void Tarjan(int u, int f, int root) {
//f为u的父亲
dfn[u] = low[u] = ++tim;
int child = 0; //记录u的孩子个数
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].to;
if (!dfn[v]) { //该点还未被访问过
child++;
Tarjan(v, u, root); //dfs过程
low[u] = min(low[u], low[v]);
//回溯时更新u的追溯值
//判断u是否为割点分两种情况:
if (u == root && child >= 2)
cut[u] = true;
//如果是dfs树的根节点且有至少两个孩子
if (u != root && low[v] >= dfn[u]) cut[u] = true;
//如果不是dfs树的根节点且孩子追溯至大于等于u的时间戳
} else if (v != f) {
//说明下一个点是祖先而不是父亲
low[u] = min(low[u], dfn[v]); //更新追溯值
}
}
}
拓扑排序
有向图拓扑排序基本步骤:
1.从图中选择一个入度为0的顶点,输出该点
2.删除该顶点及其相连的边或弧
3.重复执行1、2,直到不存在入度为0的点为止
4.若输出顶点数小于有向图顶点数,则有回路;否则输出即为一组拓扑排序序列
queue
while (!q.empty()) q.pop();
for (i = 1; i <= n; i++) {
if (du[i] == 0)
q.push(i); //依次将入度为0的点入队
}
while (!q.empty()) {
int t = q.front();
q.pop();
cout << t << " ";
//以t开始出边,遍历t所连的每一个边
for (i = head[t]; i != -1; i = e[i].next) {
int v = e[i].to;
du[v]--; //出边后入度减一
if (du[v] == 0)
q.push(v);
}
}
匈牙利算法(二分图匹配问题)
int match[N]; //与集合2中的元素i配对的是集合1中的match[i]
int ans = 0; //存储总匹配数
for (i = 1; i <= n; i++) { //遍历第一个集合的每个元素去配对
memset(v, 0, sizeof(v));
//必须清空访问数组
if (dfs(i)) ans++; //集合1中第i个元素与集合2的某元素配对成功
}
cout << ans << endl;
//输出配对关系
for(i=1;i<=n;i++){
cout<<match[i]<<"->"<<i<<endl;
}
bool dfs(int u) {
//本质是递归 用集合1的元素u去进行配对
int i;
for (i = 1; i <= n; i++) {
//遍历集合2的每个元素尝试配对
if (e[u][i] && !v[i]) {
//能匹配,且对方尚未配对
v[i] = true;//设置为已配对
if (match[i]== 0||dfs(match[i])) { // ||是短路运算符
match[i] = u;
return true; //找到 增广路 }
}
}
return false; //找不到 增广路
}
快速输入输出
inline int read() {
int t = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
t = (t << 3) + (t << 1) + (ch ^ 48);
ch = getchar();
}
return t * f;
}
inline void write(int x) {
if (x < 0) {
putchar('-');
write(-x);
} else if (x > 9) {
write(x / 10);
putchar(x % 10 + '0');
} else
putchar(x + '0');
}

浙公网安备 33010602011771号