复习资料

排序算法

选择排序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 q; //若 要求按编号大小顺序输出,则使用优先队列
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');
}
posted @ 2025-04-12 00:19  H_Elden  阅读(31)  评论(0)    收藏  举报