算法优化设计整理
第二章——算法设计优化
线性扫描、单调性问题:
第k个数问题:
1.POJ2388——nth_element()
思路分析:nth_element 函数的使用。
nth_element 用法: nth_element(a, a+k, a+n);
nth_element 时间复杂度: O(N)。
nth_element 结果:将第 k 个元素放在整个数组中的第 k 个位置,并且左侧的元素都比这个数字小,但是不保证有序,右侧都比这个元素大,同样不保证有序。
nth_element详解
#include <iostream>
#include <algorithm>
#include <cassert>
using namespace std;
const int N = 1e6+7;
int a[N];
int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
nth_element(a, a+n/2, a+n);
printf("%d\n", a[n/2]);
return 0;
}
正数连续子段和:
1.UVA1121——前缀和
题目要求:找出最短的连续子段,这个子段大于等于 S。
(N < 10,0000,S < 1,0000,0000)。
思路:连续字段和,这种连续的和,正常的复杂度是平方级别,技巧就是前缀和预处理数组的和,达到降维的目的,之后使用双指针利用正数子段和递增的性质使用双指针维护子段区间。
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1e5 + 7;
int a[N], b[N];
int main() {
int n, s;
while (scanf("%d%d", &n, &s) != EOF) {
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
b[0] = 0;
for (int i = 1; i <= n; i++) b[i] = b[i-1]+a[i];
int ans = n+1, i = 1;
for (int j = 1; j <= n; j++) {
if (b[j] - b[i] < s) continue;
while (i <= j) {
if (b[j] - b[i] < s) break;
i++;
}
ans = min(ans, j-i+1);
}
printf("%d\n", ans == n+1 ? 0: ans);
}
return 0;
}
处理连续子段和时,使用前缀和会帮到很大的忙!。
即时维护最大值:
1.UVA11078——max()维护
正常枚举是平方复杂度,但是可以看出来的是,针对于每个数字,答案就是他前边最大的数字减去他的和,也就是说每次更新最大值即可,即可优化到线性。
/*
10.35 10.43
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+7;
int a[N];
int main() {
int T, n;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int m = a[0], ans = a[0] - a[1];
for (int i = 1; i < n; i++) {
ans = max(ans, m-a[i]);
m = max(m, a[i]);
}
printf("%d\n", ans);
}
return 0;
}
Floyd 判圈:
Floyd 判圈:使用两个指针在链表上滑动,快指针的速度是慢指针的两倍,如果最后相遇,则有圈,否则无圈。
圈的入口:固定一个指针,另外一个指针回到起点,以同样的速度滑动,最后会在起点相遇。
证明结论成立,即证 a == 整数倍环长+c。
设起点到环入口距离为 a,入口到相遇点的距离为 b,相遇点到入口的距离为 c,设相遇时快指针比慢指针夺多了 k 圈,慢指针走了 m 圈。
则有等式:a+b+m(b+c) = 1/2 * (a+b+(m+k)(b+c))
化简得: 2a+2b+2m(b+c) = a+b+(m+k)(b+c)
a+b=(k-m)(b+c) = K(b+c)
证明出:a+b 等于整数倍的环长 L,也就是说 a+L-c=K*L,所以 a=(K-1)L+c,因为 K-1是整数,所以结论成立!
1.UVA11549——Floyd判圈、双指针
思路分析:可以发现,计算器的每个输出是一个状态,必定会输出另一个结果,如果只要其中的前 n 位数字,那就有可能会出现重复,因为一共就只有 10n 个数字。水平有限,无法证明具体这个循环的量级是多少,但是至少题目给 6s,应该是不用考虑数据的问题,所以正常找环即可。
取前 n 位的写法很巧妙!!!!while (xx >= top) xx/=10;
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
ll k;
ll next(ll top, ll x) {
ll xx = x*x;
while (xx >= top) xx/=10;
return xx;
}
int main() {
int T, n;
scanf("%d", &T);
while(T--) {
scanf("%d%lld", &n, &k);
ll top = 1;
for (int i = 0; i < n; i++) top = top*10;
ll ans = k, x1 = k, x2 = k;
while(1) {
x1 = next(top, x1);
x2 = next(top, x2);
if (ans < x2) ans = x2;
x2 = next(top, x2);
if (ans < x2) ans = x2;
// printf("%lld, %lld\n", x1, x2);
if (x1 == x2) break;
}
printf("%lld\n", ans);
}
return 0;
}
中途相遇二分:
将一个步骤特别多的问题,从两边分别考虑,达到从中间判定是否满足题意的思想。
1.UVA1152——equal_range()
思路:在四个集合中找到四个数字,这四个数字是分别从四个集合中取得的,需要满足四个数字之和等于 0。正常来说,是 n3 的复杂度,但只要将两个集合一起考虑,后两个集合一起考虑,即可完成 n2 的复杂度优化。
详细过程:将前两个数组每对元素的和存入一个新的数组,并排序,之后使用二分扫描区间。
equal_range:equal_range 是 C++ STL 中的一种二分查找的算法,试图在已排序的[first,last)中寻找value,它返回一对迭代器 <i,j>,其中 i 是在不破坏次序的前提下,value 可插入的第一个位置(亦即lower_bound),j 则是在不破坏次序的前提下,value可插入的最后一个位置(亦即upper_bound),因此,[i, j) 内的每个元素都等同于 value,而且 [i, j) 是 [first, last) 之中符合此一性质的最大子区间。
equal_range详解
// 2021.11.11 9.08 9.33
#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e4+7;
int a[N], b[N], c[N], d[N], sum[N*N];
int main() {
int T, n;
scanf("%d", &T);
while(T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d%d%d%d", &a[i], &b[i], &c[i], &d[i]);
int cc = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
sum[cc++] = a[i] + b[j];
sort(sum, sum+cc);
long long cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
pair<int*, int*> p = equal_range(sum,sum+cc,-c[i]-d[j]);
cnt += p.second - p.first;
}
}
printf("%lld\n", cnt);
if (T) printf("\n");
}
return 0;
}
将一个问题从前后一起考虑,头尾一起运算,中间一起在考虑,即可达到最优。
滑动窗口:
1.UVA11572——set集合、滑动窗口
滑动窗口:适用于维护连续的区间。
题目含义:找最大的区间,使得区间里每个数字都互不相同。
解决方案:相当于一个滑动窗口,只要有和右端元素相同的元素,比出现在最左端,因为时刻保证窗口中的数字都不相同,从一开始便维护这个滑动窗口。
基本操作:使用一种数据结构(队列+vis和set都可以)维护实时区间,只要需要扩充区间,只要不满足条件,就将最开始的元素删除,直至满足条件,再将新元素放置数据结构中。即可维护连续区间,在维护时实时更新最优解。
// 2021.11.11 9/58 10.12
#include <iostream>
#include <set>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e6+7;
int a[N];
int main() {
int T, n;
scanf("%d", &T);
while(T--) {
scanf("%d", &n);
set<int> st;
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
st.insert(a[0]);
int l = 0, r = 1, ans = 0;
while(r < n) {
// if (l < r && st.count(a[r])) st.erase(a[r]);
while (l < r && st.count(a[r])) st.erase(a[l++]);
st.insert(a[r++]);
ans = max(r-l, ans);
}
printf("%d\n", ans);
}
return 0;
}
2.POJ2823——单调队列(模拟双端队列)
单调队列:顾名思义,队列中所有元素单调递增,要么单调递减,是严格的!
编码技巧:保证每个元素必放进队列,这样只需要枚举队头是否区间长度小于 k 即可,之后以此队尾和欲插入元素进行比较,如果是单调递增序列,那么比当前元素大的元素都出队。
单调性判断:若是区间最小值,则可以想到,每次插入的元素一定影响着后续区间的最值,所以如果当前队列中有元素比它大,那他就没有存在的必要,因为,之前的区间由队头决定(因为单调,队头最小),而后续的最值由当前元素影响,所以他没有存在的必要,所以出队。区间最大值同理。
// 2021.11.11 10.20 10.52
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e6+7;
int q[N], a[N];
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int h = 0, r = 0;
for (int i = 0; i < n; i++) {
while (h < r && q[h] + k <= i) h++;
while (h < r && a[q[r-1]] >= a[i]) r--;
q[r++] = i;
if (i >= k - 1) printf("%d ", a[q[h]]);
}
puts("");
h = 0, r = 0;
for (int i = 0; i < n; i++) {
while(h < r && q[h] + k <= i) h++;
while(h < r && a[q[r-1]] <= a[i]) r--;
q[r++] = i;
if (i >= k - 1) printf("%d ", a[q[h]]);
}
puts("");
return 0;
}
扫描线:
1.UVA1398——扫描线
将立体二维的点区间问题,通过时间的不等式关系,判断出两个维度的时间区间,取交集,L取大的,R取小的,这样掐两头单区间的即可得到星星在相机范围的时间范围,再分别将起始时间和结束时间存入数组,排序模拟即可。
这种有明显区间开始结束,并要求取最多数量的题目,要想到将区间转化为起始结束点,之后扫描线模拟过程。
坑点:开 2 倍数组!!!!
书中还有一个点拨的地方:若想使用 int,则可以使用 lcm(1,2,3,…,10)= 2520 来是所有时间整数化。
// 11.13
#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 2e5+7;
int w, h, n;
struct node {
double x;
int type;
bool operator < (const node &a) const {
return x < a.x || (x == a.x && type > a.type);
}
}nodes[N];
// 0 < x+at < w
// a>0: -x/a<t a<0: -x/a>t
// w-x/a>t w-x/a<t
void update(int x, int a, int w, double &L, double &R) {
// if (a == 0) {
// if (x <= 0 || x >= w) R = L-1;
// }
// else if (a > 0){
// L = max(L, -(double)x/a);
// R = min(R, (double)(w-x)/a);
// }
// else {
// L = max(L, (double)(w-x/a));
// R = min(R, -(double)x/a);
// }
if (a == 0) {
if (x <= 0 || x >= w) R = L-1;
}
else if (a > 0) {
L = max(L, -(double)x/a);
R = min(R, (double)(w-x)/a);
}
else {
L = max(L, (double)(w-x)/a);
R = min(R, -(double)x/a);
}
}
int main() {
int T, x, y, a, b;
scanf("%d", &T);
while(T--) {
int e = 0, ans = 0, cnt = 0;
scanf("%d%d%d", &w, &h, &n);
for (int i = 0; i < n; i++) {
scanf("%d%d%d%d", &x, &y, &a, &b);
double L = 0, R = 1e9;
update(x,a,w,L,R);
update(y,b,h,L,R);
if (R >= L) {
nodes[e++] = {L, 0};
nodes[e++] = {R, 1};
}
}
sort(nodes, nodes+e);
for (int i = 0; i < e; i++) {
if (nodes[i].type == 0) ans = max(ans, ++cnt);
else cnt--;
}
printf("%d\n", ans);
}
return 0;
}
排序问题:
逆序对:
1.POJ1804——逆序对
模板题:用归并排序的思想。
// 11.11 15:14 15:45
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <cassert>
using namespace std;
const int N = 1e6+7;
int a[N], b[N], n;
int merge(int l, int r) { // [)
if (r - l <= 1) return 0;
int mid = (l+r)/2, ans = merge(l, mid) + merge(mid, r);
copy(a+l, a+r, b+l); // a copy b
int i = l, j = mid, k = l;
while (i < mid || j < r) {
if (i < mid && b[i] <= b[j] || j >= r) a[k++] = b[i++];
else ans += mid-i, a[k++] = b[j++];
}
return ans;
}
int main() {
int T, cnt = 0;
scanf("%d", &T);
while(T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int ans = merge(0, n);
printf("Scenario #%d:\n%d\n", ++cnt, ans);
if (T) puts("");
}
return 0;
}
动态规划:
背包DP:
01背包:
1.POJ3624——01背包滚动数组优化
//11/11 18.31 18.42
#include <algorithm>
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 2e4+7;
int w[N], v[N], dp[N];
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%d%d", &w[i], &v[i]);
for (int i = 0; i < n; i++) {
for (int j = m; j >= w[i]; j--) {
dp[j] = max(dp[j-w[i]]+v[i], dp[j]);
}
}
printf("%d\n", dp[m]);
return 0;
}
2.ACwing423——01背包模板题
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e3+7;
int dp[N], w[N], v[N];
int main() {
int n, m;
scanf("%d%d", &m, &n);
for (int i = 0; i < n; i++) scanf("%d%d", &w[i], &v[i]);
for (int i = 0; i < n; i++) {
for (int j = m; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d\n", dp[m]);
return 0;
}
3.ACwing1024——01背包应用
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e4+7;
ll dp[N], w[N];
int main() {
int n, m;
scanf("%d%d", &m, &n);
for (int i = 0; i < n; i++) scanf("%lld", &w[i]);
for (int i = 0; i < n; i++) {
for (int j = m; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j-w[i]]+w[i]);
}
}
printf("%lld\n", m - dp[m]);
return 0;
}
4.ACwing278——01背包求方案数
每次转移加上前一个状态的值,而不是方案数+1!!!
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5+7;
int dp[N];
int main() {
int n, m, x;
scanf("%d%d", &n, &m);
dp[0] = 1;
for (int i = 0; i < n; i++) {
scanf("%d", &x);
for (int j = m; j >= x; j--)
dp[j] = max(dp[j], dp[j-x]+dp[j]);
}
printf("%d\n", dp[m]);
return 0;
}
完全背包:
1.ACwing1023——完全背包求方案数
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e3+7;
int dp[N], a[] = {10, 20, 50, 100}, n;
int main() {
scanf("%d", &n);
dp[0] = 1;
for (int i = 0; i < 4; i++) {
for (int j = a[i]; j <= n; j++) {
dp[j] = max(dp[j], dp[j-a[i]]+dp[j]);
}
}
printf("%d\n", dp[n]);
return 0;
}
2.ACwing1021——完全背包求方案数
3000 不到的数据,之后 15 个面值的货币,就能超过 int 范围!!
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3e3 + 7;
typedef long long ll;
ll dp[N], n, m;
int main() {
scanf("%lld%lld", &n, &m);
dp[0] = 1;
for (int i = 0; i < n; i++) {
ll x;
scanf("%lld", &x);
for (int j = x; j <= m; j++)
dp[j] += dp[j-x];
}
printf("%lld\n", dp[m]);
return 0;
}
3.ACwing532——极大独立集、完全背包求方案数应用
性质1:a 中的数字如果能被其他数字表示,则该去除。
性质2:b 中的数字不能相互表示。
性质3:本题需要明确三个性质,b 数组一定由 a 数组中的元素取得,因为如果 b = a1+a2,因为 a1 和 a2 可以用其他的 bi 来表示,这就说明 b 可以由别的 bi 表示,不满足性质2。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3e4+7;
int dp[N], a[N], n, m;
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
sort(a, a+n);
memset(dp, 0, sizeof dp);
m = a[n-1];
int ans = 0;
dp[0] = 1;
for (int i = 0; i < n; i++) {
if (!dp[a[i]]) ans++;
for (int j = a[i]; j <= m; j++)
dp[j] += dp[j-a[i]];
}
printf("%d\n", ans);
}
return 0;
}
多重背包:
先书写一个错误代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 6e3+7;
int dp[N], n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) {
int v, w, k;
scanf("%d%d%d", &v, &w, &k);
for (int num = 0; num <= k && num*v <= m; num++) {
for (int j = m; j >= v; j--) {
dp[j] = max(dp[j], dp[j-v] + w);
}
}
}
printf("%d\n", dp[m]);
return 0;
}
如上代码,我的意图是将多重背包拆分成 k 次,更新 k 次就可以得到新的 dp 数组,但实际上,更新几次都是一样的,因为价值和消耗是一样的,更新不上去还是更新不上去,更新上去也不会再继续再原来数值的基础上再加上价值。所以仍然需要将多个物品捆绑在一起,这样才能加。
1.ACwing1019——多重背包朴素版
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 6e3+7;
int dp[N], n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) {
int v, w, k;
scanf("%d%d%d", &v, &w, &k);
for (int j = m; j >= v; j--) {
for (int l = 1; l <= k && l*v <= j; l++)
dp[j] = max(dp[j], dp[j - l*v] + l*w);
}
}
printf("%d\n", dp[m]);
return 0;
}
分组背包:
1.ACwing9——分组背包
可化简成 1 维。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e2+7;
int dp[N][N], w[N][N], v[N][N], s[N], n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &s[i]);
for (int j = 0; j < s[i]; j++) {
scanf("%d%d", &w[i][j], &v[i][j]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
dp[i][j] = dp[i - 1][j];
for (int k = 0; k < s[i]; k++) {
if (w[i][k] <= j)
dp[i][j] = max(dp[i][j], dp[i-1][j-w[i][k]] + v[i][k]);
}
}
}
printf("%d\n", dp[n][m]);
return 0;
}
有依赖的背包:
1.ACwing10——树形DP、分组背包
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e2+7;
int dp[N][N], v[N], w[N], n, m, p, root;
int h[N], e[N], ne[N], cnt;
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
void dfs(int u) {
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
dfs(j);
for (int k = m-w[u]; k >= 0; k--) {
for (int t = 0; t <= k; t++) {
dp[u][k] = max(dp[u][k], dp[u][k - t] + dp[j][t]);
}
}
}
for (int j = m; j >= w[u]; j--) dp[u][j] = dp[u][j - w[u]] + v[u];
for (int j = 0; j < w[u]; j++) dp[u][j] = 0;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &w[i], &v[i], &p);
if (p == -1) root = i;
else add(p, i);
}
dfs(root);
printf("%d\n", dp[root][m]);
return 0;
}
2.ACwing1074——分组背包、树形DP
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 2e2+7;
int dp[N][N], n, m;
int h[N], e[N], ne[N], w[N], a[N], cnt;
void add(int u, int v, int k) {
e[cnt] = v, w[cnt] = k, ne[cnt] = h[u], h[u] = cnt++;
}
void dfs1(int u, int pa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i], val = w[i];
if (v == pa) continue ;
a[v] = val;
dfs1(v, u);
}
}
void dfs(int u,int pa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == pa) continue;
dfs(v, u);
for (int j = m; j >= 0; j--)
for (int k = 0; k < j; k++)
dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k] + w[i]);
// dp[u][1] += max(dp[v][0], dp[v][1]);
// dp[u][0] += dp[v][0];
}
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 1; i <= n - 1; i++) {
int x, y, v;
scanf("%d%d%d", &x, &y, &v);
add(x, y, v);
add(y, x, v);
}
// dfs1(1, -1);
dfs(1, -1);
printf("%d\n", dp[1][m]);
return 0;
}
二维背包:
1.ACwing1022——二维背包、答案信息枚举
题目分析:最大值对应的最小体力,可以通过枚举 dp 数组得到,因为 dp 数组是单调递增的,反着枚举,第一个不是最大值的就是答案。不过有两种创建的 dp 数组的方式,第二种更快一些。
memset() 是按字节赋值!!!
memset(dp, 0x3f, sizeof dp) 最后结果是 0x3f3f3f3f;
memset(dp, 2000, sizeof dp) 最后是 4 个 (2000 取前 8 位的数字)。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e3+7;
int dp[N][N], w[N], beat[N];
int main() {
int n, m, k;
scanf("%d%d%d", &k, &m, &n);
for (int i = 0; i < n; i++) scanf("%d%d", &w[i], &beat[i]);
for (int i = 0; i < n; i++)
for (int j = k; j >= w[i]; j--)
for (int l = m-1; l >= beat[i]; l--)
dp[j][l] = max(dp[j][l], dp[j-w[i]][l-beat[i]] + 1);
int ii = m-1;
while (ii && dp[k][ii - 1] == dp[k][m - 1]) ii--;
printf("%d %d\n", dp[k][m-1], m - ii);
return 0;
}
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e3+7;
int dp[N][N], w[N], beat[N], n, m, k;
int main() {
scanf("%d%d%d", &k, &m, &n);
for (int i = 0; i < n; i++) scanf("%d%d", &w[i], &beat[i]);
memset(dp, 0x3f, sizeof dp);
// printf("%x\n", dp[2][3]);
dp[0][0] = 0;
for (int i = 0; i < n; i++) {
for (int j = m-1; j >= beat[i]; j--) {
for (int l = n; l >= 1; l--) {
if (dp[j-beat[i]][l-1] + w[i] > k) continue;
dp[j][l] = min(dp[j][l], dp[j-beat[i]][l-1] + w[i]);
}
}
}
for (int i = n; i >= 0; i--) {
for (int j = 0; j <= m-1; j++) {
if (dp[j][i] != 0x3f3f3f3f) {
printf("%d %d\n", i, m - j);
return 0;
}
}
}
return 0;
}
区间DP:
1.ACwing252——区间DP、前缀和、贪心
思路分析:因为代价最小,一个很重要的条件就是,不会合并原始的一堆石子超过两次,否则会有交叉,必然会更多,所以可以优化到三重循环的DP做!这是因为前缀和可以帮我们剩下一层复杂度,还有枚举区间的复杂度一共两层循环。
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 307;
int dp[N][N], a[N];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// for (int i = 1; i <= n; i++) dp[1][i] = a[i];
for (int i = 2; i <= n; i++) a[i] += a[i-1];
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= n-i+1; j++) {
dp[i][j] = 1e7;
for (int k = 1; k <= i - 1; k++) {
dp[i][j] = min(dp[i][j], dp[k][j]+dp[i-k][j+k]+a[j+i-1]-a[j-1]);
}
}
}
printf("%d\n", dp[n][1]);
return 0;
}
树形DP:
极大独立集:
1.ACwing285没有上司的舞会——树形DP、求最大和
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
#include <algorithm>
using namespace std;
const int N = 6e3+7;
int dp[N][2], a[N], n;
int h[N], e[N], ne[N], cnt, root;
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
set<int>st;
void dfs(int u) {
dp[u][1] = a[u];
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
dfs(v);
dp[u][0] += max(dp[v][0], dp[v][1]);
dp[u][1] += dp[v][0];
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i++) {
int x, y;
scanf("%d%d", &x, &y);
add(y, x);
st.insert(x);
}
for (int i = 1; i <= n; i++) {
if (st.count(i)) continue;
else {
root = i;
break;
}
}
dfs(root);
printf("%d\n", max(dp[root][0], dp[root][1]));
return 0;
}
2.ACwing323战略游戏——树形DP、求最小和
1 个结点时,需要注意没有边,所以不需要士兵。
#include <iostream>
#include <cstdio>
#include <set>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3e3+7;
set<int>st;
int v[N], dp[N][2];
int h[N], e[N], ne[N], cnt, root;
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
void dfs(int u) {
dp[u][0] = 0;
dp[u][1] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
dfs(vv);
dp[u][0] += dp[vv][1];
dp[u][1] += min(dp[vv][1], dp[vv][0]);
// dp[u][0] += dp[vv][1];
// dp[u][1] += max(dp[vv][1] - 1, dp[vv][0]);
}
}
int main() {
int n, x, s, y;
while(scanf("%d", &n) == 1) {
cnt = 0;
memset(h, -1, sizeof h);
st.clear();
for (int i = 0; i < n; i++) {
scanf("%d:(%d)", &x, &s);
for (int j = 0; j < s; j++) {
scanf("%d", &y);
st.insert(y);
add(x, y);
}
}
for (int i = 0; i < n; i++) {
if (st.count(i)) continue;
else root = i;
// st.erase(i);
}
// printf("root = %d\n", root);
dfs(root);
if (n == 1) {
puts("0");
continue ;
}
printf("%d\n", min(dp[root][0], dp[root][1]));
}
return 0;
}
3.ACwing1077皇宫看守——复杂状态的树形DP
分为三个状态。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <cstring>
#include <vector>
using namespace std;
const int N = 3e3+7;
int dp[N][4], w[N], n;
int h[N], e[N], ne[N], cnt, root;
set<int> st;
vector<int> ve[N];
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
// void dfs(int u) {
// dp[u][0] = 0;
// dp[u][1] = w[u];
// for (int i = h[u]; ~i; i = ne[i]) {
// int vv = e[i];
// dfs(vv);
// dp[u][0] += dp[vv][1];
// dp[u][1] += min(dp[vv][0], dp[vv][1]);
// }
// }
// void dfs(int u) {
// dp[u][0] = 0;
// dp[u][1] = w[u];
// for (int i = h[u]; ~i; i = ne[i]) {
// int vv = w[i];
// ve[u].push_back(vv);
// dfs(vv);
// dp[u][0] += dp[vv][1];
// if (ve[vv].size())
// for (auto gs: ve[vv])
// dp[u][1] += min(dp[gs][1], dp[vv][1]);
// else dp[u][1] += dp[vv][0];
// }
// }
// 0: 不放,被爸爸看到,1:不放,被儿子看到,2:放
int a[N];
void dfs(int u) {
dp[u][0] = 0;
dp[u][2] = w[u];
int sum = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
dfs(vv);
dp[u][0] += min(dp[vv][1], dp[vv][2]);
dp[u][2] += min(dp[vv][1], min(dp[vv][0], dp[vv][2]));
a[vv] = min(dp[vv][1], dp[vv][2]);
sum += a[vv];
}
dp[u][1] = 1e8;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
dp[u][1] = min(dp[vv][2] + sum - a[vv], dp[u][1]);
}
}
int main() {
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++) {
int s, x, y;
scanf("%d%d%d", &x, &y, &s);
w[x] = y;
for (int j = 0; j < s; j++) {
scanf("%d", &y);
st.insert(y);
add(x, y);
}
}
for (int i = 1; i <= n; i++) {
if (st.count(i)) continue;
root = i;
break;
}
dfs(root);
if (n == 1) {
printf("%d\n", w[1]);
return 0;
}
if (n == 2) {
printf("%d\n", min(w[1], w[2]));
return 0;
}
printf("%d\n", min(dp[root][2], dp[root][1]));
return 0;
}
树的最长路径:
每棵树的最长路径是每个结点的第一深路径和第二深路径的和。
一边深搜,全局维护最优解即可。
1.ACwing1072——树的最长路径模板题
// 11/11 19.56 20.29
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e4+7;
int e[N], h[N], p[N], ne[N], cnt, sum;
void add(int u, int v, int w) {
e[cnt] = v, ne[cnt] = h[u], p[cnt] = w, h[u] = cnt++;
}
int dfs(int u, int pa) {
// printf("%d\n", u);
int ans = 0, ans2 = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == pa) continue;
int s = dfs(v, u) + p[i];
if (s > ans) ans2 = ans, ans = s;
else ans2 = max(ans2, s);
}
sum = max(sum, ans+ans2);
return ans;
}
int main() {
int n, u, v, w;
memset(h, -1, sizeof h);
scanf("%d", &n);
for (int i = 0; i < n-1; i++) {
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
dfs(1, -1);
printf("%d\n", sum);
return 0;
}
2.ACwing1075——约束和NlogN处理,无根树最长路径
题目分析:难点在于快速求出数字的约束和,先枚举每个数字,之后枚举倍数来找到他的所有倍数,每次枚举到倍数,相应的倍数的sum数组就加上i。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1e5+7;
int dp[N], n, m;
int h[N], e[N], ne[N], sum[N], cnt, ans;
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
int dfs(int u, int pa) {
int d1 = 0, d2 = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == pa) continue;
int s = dfs(j, u) + 1;
if (s >= d1) d2 = d1, d1 = s;
else if (s > d2) d2 = s;
}
ans = max(d1+d2, ans);
return d1;
}
int main() {
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++)
for (int j = 2; j <= n/i; j++)
sum[i*j] += i;
for (int i = 2; i <= n; i++)
if (sum[i] < i) add(i, sum[i]), add(sum[i], i);
dfs(1, -1);
printf("%d\n", ans);
return 0;
}
树的中心:
树上的一个结点:到其他结点的最远距离最小。
1.ACwing1073——上下深搜
题目分析:一棵树的一个结点到其他结点距离最远,有三种情况:
- 其子树的最远距离(求法见树的最长路径)。
- 其父结点可到的最远距离的点:
(1)父亲结点向上搜索的最远距离+连接父子两点的边权:父亲结点的这个状态可以根据其祖父结点的向上搜索和向下搜索的最远距离+连接该点的边权进行更新,而动态规划向上搜索的初始状态就是根节点的向上搜索最远距离,即 0(dp_up[root] = 0)。编码时从根往下深搜即可。
(2)父亲结点向下搜索的最远距离+连接父子两点的边权:这个在 1 中已经全部计算完毕。
编程思路:1 的向上搜索是用子结点的信息更新父结点,2 的向下搜索是用父结点的信息更新子节点。因为1是自底向上,2 是自顶向下。
本题的负权会误导初始化成-inf,其实0就可以,因为如果最远距离为负,那还不如哪也不去,就自己到自己的距离为 0,都比最远距离远。就相当于负权图,累计为负,所以不累计就是最佳选择!
// 11/14 8.06
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5+7;
const int inf = 0x3f3f3f3f;
int d1[N], d2[N], up[N];
int h[N], e[N], ne[N], w[N], p[N], cnt;
// 不初始化 up[] 为 -inf 是因为结点的向上搜索距离如果为负,还不如自己到自己自己最远,就是 0
// d[] 数组不初始化为 -inf 同理
void init() {
memset(h, -1, sizeof h);
}
void add(int u, int v, int w_) {
e[cnt] = v, ne[cnt] = h[u], w[cnt] = w_, h[u] = cnt++;
}
int dfs_d(int u, int pa) {
// d1[u] = d2[u] = -inf;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (pa == v) continue;
int s = dfs_d(v, u)+w[i];
if (s >= d1[u])
d2[u] = d1[u], d1[u] = s, p[u] = v;
else if (s >= d2[u]) d2[u] = s;
}
// if (d1[u] == -inf) d1[u] = d2[u] = 0;
// else if (d2[u] == -inf) d2[u] = 0;
return d1[u];
}
void dfs_u(int u, int pa) {
if (pa == -1) up[u] = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == pa) continue;
up[v] = max(up[v], up[u]+w[i]);
if (p[u] == v) up[v] = max(up[v], d2[u]+w[i]);
else up[v] = max(up[v], d1[u]+w[i]);
dfs_u(v, u);
}
}
int main() {
int n;
scanf("%d", &n);
init();
for (int i = 0; i < n-1; i++) {
int u,v,w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
dfs_d(1, -1);
dfs_u(1, -1);
int ans = inf;
for (int i = 1; i <= n; i++) ans = min(ans, max(d1[i],up[i]));
printf("%d\n", ans);
return 0;
}
第四章——数据结构
堆(Heap)
平衡树
Treap
- 中序遍历为从小到大排列。
- 找一个结点前驱的方法:
(1)如果有左子树:从左孩子开始,一直往右走。
(2)如果不存在左子树:第一个出现的左上边通向的结点。这说明该结点是某个结点的右子树的最小值,所以该结点的前驱就是这个结点。根据二叉搜索树的性质,从该结点一直顺着父亲结点往上走,只要出现往左拐的情况,那么该边通向的结点就是第一个比它小的元素,因为往上走的时候,往右上走,到达的都是比它大的元素,只有往左上走的时候,才是第一个遇到的比它小的元素。这个元素即为它的前驱。 - 找一个结点后继的方法:
(1)如果有右子树:从右孩子开始,一直往左走。
(2)如果不存在右子树:第一个出现的右上边通向的结点。
Splay
- 双旋上升,使得一个结点一次上升两层,如果没有折线,则先转该点父亲,再转该点,否则,转两次该点。
- 双旋比起单旋,避免了链表的情况,所以,没有直线的时候,先转他的父亲,保证减少一层之后,再转本身。(三层以上有效,不包含三层)。
- 并非严格平衡,因为每次只是将上次查找的结点转到根节点,,如果是最小的元素转到根节点,这很明显就不是一颗平衡树。
- 时间复杂度:O((M+N)log(N)。(m 为操作数,n 为节点总数)。
Splay时间复杂度详细分析1
Splay时间复杂度详细分析2
1.ACwing253——Splay模板题前驱后继删除
// 11/13 19.35 20.13
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
const int inf = 1e8;
int id[N], n, m, cnt, root;
void init() {
cnt = 0;
for (int i = n+2; i >= 1; i--) id[cnt++] = i;
}
struct node {
int s[2], p, v, count, size;
node(){}
node(int p_, int v_): p(p_), v(v_) {
s[0] = s[1] = 0, count = size = 1;
}
}tr[N];
void pushup(int x) {
tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + tr[x].count;
}
void rotate(int x) {
int y = tr[x].p, z = tr[y].p;
int k = tr[y].s[1] == x, kk = tr[z].s[1] == y;
tr[z].s[kk] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k^1], tr[tr[x].s[k^1]].p = y;
tr[x].s[k^1] = y, tr[y].p = x;
pushup(y), pushup(x);
}
void splay(int x, int k) {
while (tr[x].p != k) {
int y = tr[x].p, z = tr[y].p;
if (z != k)
(tr[y].s[1]==x)^(tr[z].s[1]==y)?rotate(x):rotate(y);
rotate(x);
}
if (!k) root = x;
}
void insert(int x) {
int u = root, p = 0, k;
while (u && x != tr[u].v) p=u, k=x>tr[u].v, u = tr[u].s[k];
if (u) tr[u].count++;
else {
u = id[--cnt]; tr[u] = node(p, x);
if (p) tr[p].s[k] = u;
}
splay(u, 0);
}
void findx(int x) {
int u = root;
if (!u) return ; // tree empty
while (tr[u].s[x>tr[u].v] && x!=tr[u].v)
u = tr[u].s[x>tr[u].v];
splay(u, 0);
}
int getk(int k) {
int u = root;
if (tr[u].size < k) return 0;
while (u) {
int l = tr[u].s[0], r = tr[u].s[1];
if (k > tr[l].size + tr[u].count)
k -= tr[l].size + tr[u].count, u = r;
else if (k <= tr[l].size) u = l;
else break;
}
return u; // 查找成功
}
int nex(int x, int f) {
findx(x);
int u = root;
if (f && tr[u].v>x) return u;
if (!f && tr[u].v<x) return u;
u = tr[u].s[f];
while(tr[u].s[f^1]) u=tr[u].s[f^1];
return u;
}
void dele(int x) {
int l = nex(x, 0), r = nex(x, 1);
splay(l, 0), splay(r, l);
int u = tr[r].s[0];
tr[u].count--;
if (!tr[u].count) id[cnt++] = u, tr[r].s[0] = 0;
else splay(u, 0);
}
int main() {
scanf("%d", &n);
init();
insert(-inf), insert(inf);
int op, x;
for (int i = 0; i < n; i++) {
scanf("%d%d", &op, &x);
if (op == 1) insert(x);
else if (op == 2) dele(x);
else if (op == 3) {
findx(x);
printf("%d\n", tr[tr[root].s[0]].size);
}
else if (op == 4) {
int u = getk(x+1);
printf("%d\n", tr[u].v);
}
else if (op == 5) {
int u = nex(x, 0);
printf("%d\n", tr[u].v);
}
else {
int u = nex(x, 1);
printf("%d\n", tr[u].v);
}
}
return 0;
}
2.ACwing256——Splay简单应用
// 11/13 20.44 ]]]] 20.56
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 4e4+7;
const int inf = 1e8;
int id[N], cnt, root, n;
void init() {
cnt = 0;
for (int i = n+2; i >= 1; i--) id[cnt++] = i;
}
struct node {
int s[2], p, v, size;
node(){}
node(int p_, int v_): p(p_), v(v_) {
s[0] = s[1] = 0, size = 1;
}
}tr[N];
void pushup(int x) {
tr[x].size = tr[tr[x].s[0]].size + 1 + tr[tr[x].s[1]].size;
}
void rotate(int x) {
int y = tr[x].p, z = tr[y].p;
int k = tr[y].s[1] == x, kk = tr[z].s[1] == y;
tr[z].s[kk] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k^1], tr[tr[x].s[k^1]].p = y;
tr[x].s[k^1] = y, tr[y].p = x;
pushup(y), pushup(x);
}
void splay(int x, int k) {
while (tr[x].p != k) {
int y = tr[x].p, z = tr[y].p;
if (z != k)
(tr[y].s[0]==x)^(tr[z].s[0]==y)? rotate(x): rotate(y);
rotate(x);
}
if (!k) root = x;
}
void insert(int x) {
int u = root, p = 0, k;
while (u && x != tr[u].v) {
p = u, k = x>tr[u].v;
u = tr[u].s[k];
}
if (!u) {
u = id[--cnt], tr[u] = node(p, x);
if (p) tr[p].s[k] = u;
}
splay(u, 0);
}
void findx(int x) {
int u = root;
if (!u) return ;
while (tr[u].s[x>tr[u].v] && x != tr[u].v)
u = tr[u].s[x>tr[u].v];
splay(u, 0);
}
int nex(int x, int f) {
findx(x);
int u = root;
// printf("u=%d\n", u);
if (!f && tr[u].v <= x) return u;
if (f && tr[u].v >= x) return u;
u = tr[u].s[f];
while (tr[u].s[f^1]) u = tr[u].s[f^1];
return u;
}
void output(int u) {
if (!u) return ;
output(tr[u].s[0]);
if (tr[u].v > -inf && tr[u].v <= inf)
printf("%d ", tr[u].v);
output(tr[u].s[1]);
}
int main() {
scanf("%d", &n);
init();
int x, ans = 0;
insert(-inf), insert(inf);
for (int i = 0; i < n; i++) {
scanf("%d", &x);
if (!i) ans += x;
else {
int l = nex(x, 0), r = nex(x, 1);
ans += min(abs(x-tr[l].v), abs(x-tr[r].v));
// printf("l=%d, r=%d, ans=%d\n",
// l, r, min(abs(x-tr[l].v), abs(x-tr[r].v)));
}
insert(x);
// output(root);
// puts("");
}
printf("%d\n", ans);
return 0;
}
3.ACwing1063——并查集、Splay合并
Splay合并操作:Nlog2N 的复杂度,记录每个 Splay 根的数组 root[N] 只记录并查集维护的祖先。所以函数只传 b,不传 root[b]。因为不是维护祖先所在 Splay 的根再根(因为 Splay 结点的序号不等价于题目中结点序号,所以只传并查集维护的祖先),而是维护祖先结点所在 Splay 的根。
并查集:维护两个结点的连通性。如果连通,不合并;否则使用启发式合并将小的树合并到大的树;
Splay合并:每次插入时,insert()传入的应该是 b,这个结点,而不是 root[b],否则 insert 内部会再取一遍 root,变成 root[root[b]],这里 root[b] 代表并查集中祖先结点所在 Splay 的根的序号,而再取一遍 root,会变成 Splay 中结点的序号所在的 Splay 的根,而初始化的根是结点序号,并非是 Splay 结点所分配的序号,这是难点。
#include <iostream> // 8.52
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1e5+7;
struct node {
int idx, v, p, s[2], count, size;
node(){}
node(int idx_, int v_, int p_): idx(idx_), v(v_), p(p_) { count = size = 1; s[0] = s[1] = 0;}
}tr[N];
int p[N], ids[N], idc, root[N], n, m;
void init() {
for (int i = N-1; i >= 1; i--) ids[idc++] = i;
}
int find(int x) {
return x == p[x] ? x : p[x] = find(p[x]);
}
void pushup(int x) {
tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + tr[x].count;
}
void rotate(int x) {
int y = tr[x].p, z = tr[y].p;
int k = x == tr[y].s[1], kk = y == tr[z].s[1];
tr[z].s[kk] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k^1], tr[tr[x].s[k^1]].p = y;
tr[x].s[k^1] = y, tr[y].p = x;
pushup(y), pushup(x);
}
void splay(int x, int k, int b) {
while (tr[x].p != k) {
int y = tr[x].p, z = tr[y].p;
if (z != k) (tr[y].s[1] == x)^(tr[z].s[1] == y) ? rotate(x): rotate(y);
rotate(x);
}
if (!k) root[b] = x;
}
void insert(int x, int id, int b) {
int u = root[b], p;
while(u && x != tr[u].v) p = u, u = tr[u].s[x>tr[u].v];
if (u) tr[u].count++;
else {
u = ids[--idc];
tr[u] = node(id, x, p);
if (p) tr[p].s[x>tr[p].v] = u;
}
splay(u, 0, b);
}
int getk(int k, int b) {
int u = root[b];
while (u) {
int l = tr[u].s[0], r = tr[u].s[1];
if (k > tr[l].size + tr[u].count) k -= tr[l].size + tr[u].count, u = r;
else if (k <= tr[l].size) u = l;
else return u;
}
return 0;
}
void dfs(int u, int b) {
if (tr[u].s[0]) dfs(tr[u].s[0], b);
if (tr[u].s[1]) dfs(tr[u].s[1], b);
insert(tr[u].v, tr[u].idx, b);
ids[idc++] = u;
}
int main() {
scanf("%d%d", &n, &m);
init();
for (int i = 1; i <= n; i++) {
p[i] = root[i] = i;
int v;
scanf("%d", &v);
tr[ids[--idc]] = node(i, v, 0); // 0 代表 没有父结点,即根结点
}
while(m--) {
int a, b;
scanf("%d%d", &a, &b);
a = find(a), b = find(b);
if (tr[root[a]].size > tr[root[b]].size) swap(a, b);
if (a != b) dfs(root[a], b);
p[a] = b;
}
char op[3];
int x, k;
scanf("%d", &m);
while (m--) {
scanf("%s%d%d", op, &x, &k);
if (*op == 'B') {
x = find(x), k = find(k);
if (tr[x].size > tr[k].size) swap(x, k);
if (x != k) dfs(root[x], k);
p[x] = k;
}
else {
x = find(x);
if (k > tr[root[find(x)]].size) puts("-1");
else {
printf("%d\n", getk(k, find(x)));
}
}
}
return 0;
}
分块
将整个区间分成 logN 段,分别使用数组维护段内的数值。
完整段:直接数组维护,暂不操作整个段(懒标记)。
段内:暴力枚举维护。O(logN)。
实现手段:设置映射:完成点到段的索引获取。i/len(sqrt(N))。
1.ACwing243——分块求和
#include <iostream>
#include <algorithm>
// #include <cstring>
#include <cmath>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 1e5+7, M = 350;
ll w[N], add[M], sum[M];
int len, n, m;
int get(int x) {
return x/len;
}
void change(int l, int r, int d) {
if (get(l) == get(r)) {
for (int i = l; i <= r; i++) w[i] += d, sum[get(i)] += d;
}
else {
int i = l, j = r;
while (get(i) == get(l)) w[i] += d, sum[get(i)] += d, i++;
while (get(j) == get(r)) w[j] += d, sum[get(j)] += d, j--;
for (int k = get(i); k <= get(j); k++) add[k] += d, sum[k] += len*d;
}
}
ll query(int l, int r) {
ll ans = 0;
if (get(l) == get(r))
for (int i = l; i <= r; i++) ans += w[i] + add[get(i)];
else {
int i = l, j = r;
while (get(i) == get(l)) ans += w[i] + add[get(i)], i++;
while (get(j) == get(r)) ans += w[j] + add[get(j)], j--;
for (int k = get(i); k <= get(j); k++) ans += sum[k];
}
return ans;
}
int main() {
scanf("%d%d", &n, &m);
len = sqrt(n);
for (int i = 1; i <= n; i++) {
scanf("%lld", &w[i]);
sum[get(i)] += w[i];
}
char op[2];
int l, r, d;
while (m--) {
scanf("%s%d%d", op, &l, &r);
if (*op == 'C') {
scanf("%d", &d);
change(l, r, d);
}
else {
ll t = query(l, r);
printf("%lld\n", t);
}
}
return 0;
};
分块——块状链表
图论
最大流
最大流判定(二分框架)
1.ACwing2277——最大流判定、二分
1.根据二分枚举答案区间,如果dinic() >= K,就缩小答案,直到无解,如果无解,扩大答案,知道满足答案。
2.每条边流量如果大于答案,就是0,不能走,如果小于等于答案,就是1,代表可以走。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int N = 8e4+7, INF = 1e8;
int h[N], e[N], f[N], w[N], ne[N], cur[N], d[N], cnt;
int n, m, S, T, K;
void add(int u, int v, int c) {
e[cnt] = v, w[cnt] = c, ne[cnt] = h[u], h[u] = cnt++;
e[cnt] = u, w[cnt] = c, ne[cnt] = h[v], h[v] = cnt++;
}
bool bfs() {
int q[N], hh = 0, tt = -1;
memset(d, -1, sizeof(d));
q[++tt] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (d[vv] == -1 && f[i]) {
cur[vv] = h[vv];
d[vv] = d[u] + 1;
if (vv == T) return true;
q[++tt] = vv;
}
}
}
return false;
}
int dfs(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i]) {
int vv = e[i];
cur[u] = i;
if (d[vv] == d[u] + 1 && f[i]) {
int t = dfs(vv, min(limit - flow, f[i]));
if (!t) d[vv] = -1;
else f[i] -= t, f[i^1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int flow = 0, r = 0;
while (bfs()) while(flow = dfs(S, INF)) r += flow;
// printf("flow=%d\n", r);
return r;
}
bool check(int mid) {
for (int i = 0; i < cnt; i++)
if (w[i] > mid) f[i] = 0;
else f[i] = 1;
return dinic() >= K;
}
int main() {
scanf("%d%d%d", &n, &m, &K);
S = 1, T = n;
memset(h, -1, sizeof(h));
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int l = 1, r = 1e6;
while (l < r) {
int mid = (l+r)>>1;
if (check(mid)) r = mid;
else l = mid + 1;
// printf("check=%d\n", check(mid));
}
printf("%d\n", r);
return 0;
}
分层图
1.ACwing2187——分层图、顺序枚举最大流判定
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = (22*50*2)*400+2, INF = 1e8+7;
int h[N], e[N], ne[N], f[N], cur[N], d[N], p[N], cnt;
int n, m, S, T, K;
void add(int u, int v, int c) {
e[cnt] = v, f[cnt] = c, ne[cnt] = h[u], h[u] = cnt++;
e[cnt] = u, f[cnt] = 0, ne[cnt] = h[v], h[v] = cnt++;
}
struct Ship{
int h, r, ids[30];
}ships[30];
int find(int x) {
return x == p[x] ? p[x] : p[x] = find(p[x]);
}
int get(int day, int i) {
return day*(n+2) + i;
}
bool bfs() {
memset(d, -1, sizeof(d));
int q[N], hh = 0, tt = -1;
q[++tt] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (d[vv] == -1 && f[i]) {
d[vv] = d[u] + 1;
cur[vv] = h[vv];
if (vv == T) return true;
q[++tt] = vv;
}
}
}
return false;
}
int dfs(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i]) {
int vv = e[i];
cur[u] = i;
if (d[vv] == d[u] + 1 && f[i]) {
int t = dfs(vv, min(f[i], limit - flow));
if (!t) d[vv] = -1;
else f[i] -= t, f[i^1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int flow, r = 0;
while(bfs()) while(flow = dfs(S, INF)) r += flow;
return r;
}
int main() {
scanf("%d%d%d", &n, &m, &K);
for (int i = 0; i <= n+1; i++) p[i] = i;
S = N - 2, T = N - 1;
memset(h, -1, sizeof(h));
for (int i = 0; i < m; i++) {
int h, r;
scanf("%d%d", &h, &r);
ships[i] = {h, r};
for (int j = 0; j < r; j++) {
int id;
scanf("%d", &id);
if (id == -1) id = n+1;
ships[i].ids[j] = id;
if (j)
p[find(id)] = find(ships[i].ids[j-1]); // !!
// printf("%d ", ships[i].ids[j]);
}
// puts("");
}
// for (int i = 0; i <= n + 1; i++)
// printf("p[%d] = %d\n", i, p[i]);
if (find(0) != find(n+1)) puts("0");
else {
int day = 0, ans = 0;
add(S, get(day, 0), K);
// printf("S=%d\n", S);
add(get(day, n+1), T, INF);
while (++day) {
// printf("day=%d\n", day);
add(get(day, n+1), T, INF);
for (int i = 0; i <= n+1; i++)
add(get(day-1, i), get(day, i), INF);
for (int i = 0; i < m; i++) {
int r = ships[i].r;
int a = ships[i].ids[(day-1)%r], b = ships[i].ids[day%r];
add(get(day-1, a), get(day, b), ships[i].h);
// printf("%d %d\n", a, b);
}
ans += dinic();
// printf("ans=%d, k=%d\n", ans, K);
if (ans >= K) break;
// printf("%d\n", ans);
// if (day >= 10) break;
}
printf("%d\n", day);
}
return 0;
}
拆点
1.ACwing2240——拆点
当点的数量受限制时,将点拆成出点和入点两个点,中间用流量限制数量即可。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 507, M = 1000000+307, INF = 1e8+7;
int h[N], e[M], ne[M], f[M], cur[N], d[N], cnt;
int S, T, n, m, F, D;
void add(int u, int v, int c) {
e[cnt] = v, f[cnt] = c, ne[cnt] = h[u], h[u] = cnt++;
e[cnt] = u, f[cnt] = 0, ne[cnt] = h[v], h[v] = cnt++;
}
bool bfs() {
memset(d, -1, sizeof(d));
int q[N], hh = 0, tt = -1;
q[++tt] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt) {
int u = q[hh++];
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (d[vv] == -1 && f[i]) {
d[vv] = d[u] + 1;
cur[vv] = h[vv];
if (vv == T) return true;
q[++tt] = vv;
}
}
}
return false;
}
int dfs(int u, int limit) {
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i]) {
int vv = e[i];
cur[u] = i;
if (d[vv] == d[u] + 1 && f[i]) {
int t = dfs(vv, min(f[i], limit - flow));
if (!t) d[vv] = -1;
else f[i] -= t, f[i^1] += t, flow += t;
}
}
return flow;
}
int dinic() {
int flow, r = 0;
while(bfs()) while(flow = dfs(S, INF)) r += flow;
return r;
}
int main() {
scanf("%d%d%d", &n, &F, &D);
memset(h, -1, sizeof(h));
S = 0, T = 2*n + F + D + 1;
for (int i = 1; i <= n; i++) {
int ff, dd, x;
scanf("%d%d", &ff, &dd);
for (int j = 1; j <= ff; j++) {
scanf("%d", &x);
add(x, F + i, 1);
}
add(F + i, F + i + n, 1);
for (int j = 1; j <= dd; j++) {
scanf("%d", &x);
add(F + i + n, F + 2*n + x, 1);
}
}
for (int i = 1; i <= F; i++) add(S, i, 1);
for (int i = F + 2*n + 1; i <= F + 2*n + D; i++) add(i, T, 1);
printf("%d\n", dinic());
return 0;
};
有向图的强连通分量
1.ACwing1174——tarjan、缩点法
将每个强连通分量看作一个点,并记录这个连通分量中点的个数,另外还需要记录每个强连通分量到其他强连通分量的出度,这样最后判断出度为 0 的点是否只有一个即可。一个的话,输出那个点代表的强连通分量中点的个数。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5+7;
int h[N], e[N], ne[N], cnt, n, m;
int stk[N], top, in_stk[N], dfn[N], low[N], id[N], Size[N], dout[N], scc_cnt, timestep;
void init() {
cnt = scc_cnt = timestep = top = 0;
memset(h, -1, sizeof(h));
memset(Size, 0, sizeof(Size));
memset(dout, 0, sizeof(dout));
}
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
void tarjan(int u) {
dfn[u] = low[u] = ++timestep;
stk[top++] = u, in_stk[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (!dfn[vv]) {
tarjan(vv);
low[u] = min(low[u], low[vv]);
} else if (in_stk[vv]) low[u] = min(low[u], dfn[vv]);
}
if (dfn[u] == low[u]) {
while (top) {
int vv = stk[--top];
in_stk[vv] = 0;
id[vv] = scc_cnt;
Size[scc_cnt]++;
if (dfn[vv] == low[vv]) break;
}
scc_cnt++;
}
}
int main() {
scanf("%d%d", &n, &m);
init();
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i);
for (int i = 1; i <= n; i++) {
for (int j = h[i]; ~j; j = ne[j]) {
int vv = e[j];
if (id[i] != id[vv]) dout[id[i]]++;
}
}
int zero = 0, ans = 0;
for (int i = 0; i < scc_cnt; i++) {
if (!dout[i]) {
ans = Size[i];
zero++;
if (zero > 1) {
ans = 0;
break;
}
}
}
printf("%d\n", ans);
return 0;
}
无向图的双连通分量
边的双连通分量
根据桥缩点,直接枚举边即可。
1.ACwing395——边的双连通分量、缩点
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2e4+7;
int h[N], e[N], ne[N], cnt, n, m;
int dfn[N], low[N], stk[N], id[N], is_edge[N], din[N], top, dcc_cnt, timestep;
void init() {
cnt = 0;
memset(h, -1, sizeof h);
}
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++timestep;
stk[top++] = u;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (vv == fa) continue;
if (!dfn[vv]) {
tarjan(vv, u);
if (low[vv] > dfn[u]) is_edge[i] = is_edge[i^1] = true;
low[u] = min(low[u], low[vv]);
}
low[u] = min(low[u], low[vv]);
}
if (dfn[u] == low[u]) {
while (top) {
int vv = stk[--top];
id[vv] = dcc_cnt;
if (dfn[vv] == low[vv]) break;
}
dcc_cnt++;
}
}
int main() {
scanf("%d%d", &n, &m);
init();
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
add(b, a);
}
for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i, 0);
// for (int u = 1; u <= n; u++) {
// for (int i = h[u]; ~i; i = ne[i]) {
// int vv = e[i];
// din[id[vv]]++;
// }
// }
for (int i = 0; i < cnt; i++)
if (is_edge[i]) din[id[e[i]]]++;
int ans = 0;
for (int i = 0; i < dcc_cnt; i++) {
if (din[i] == 1) ans++;
}
printf("%d\n", (ans+1)/2);
return 0;
}
数学
扩展欧几里得定理
推导:ax+by = gcd(a, b) = gcd(b, a%b)
所以得到: by+(a%b)x --> by+(a-a/b * b)x
整理得到: ax+b(y-a/b*x) = gcd(a,b)
1.ACwing877——扩展欧几里得定理
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int gcd(int &x, int &y, int a, int b) {
if (!b) {
x = 1, y = 0;
return a;
}
int d = gcd(y, x, b, a%b); // ax+by=d by+(a-a/b*b)x=d
y = y - a/b*x;
return d;
}
int main() {
int x, y, a, b;
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &a, &b);
gcd(x, y, a, b);
printf("%d %d\n", x, y);
}
return 0;
}
2.ACwing878——线性同余方程
题目理解:使用扩展欧几里得算法求出 x,但是对应到 b 需要 b 是最大公约数的倍数,且扩大的过程中可能超过 int 范围,所以需要最后模 m。
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
// ax+by=d by+(a-a/b*b)x=d
int gcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1, y = 0;
return a;
}
int d = gcd(b, a%b, y, x);
y -= a/b*x;
return d;
}
// ax-my=b
int main() {
int T, a, b, m, x, y, d;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &a, &b, &m);
if(b%(d = gcd(a, m, x, y)) != 0) puts("impossible");
else printf("%d\n", 1ll*x*(b/d)%m);
}
return 0;
}
欧拉函数
证明:根据唯一分解定理,容斥原理。
f(N)=N(1-1/p1)(1-1/p2)…(1-1/pk)。
1.ACwing873——欧拉函数
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int main() {
int n, T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
int res = n;
for (int i = 2; i <= n/i; i++)
if (n%i == 0) {
res = res/i*(i-1);
while (n%i == 0) n /= i;
}
if (n > 1) res = res/n*(n-1);
printf("%d\n", res);
}
return 0;
}