决策单调性优化
前置知识
决策单调性是在最优化 DP 中的可能出现的一种性质。
决策单调性
对于形如:
fi=i−1minj=0{fj+w(j,i)}
的转移方程,记 pi 为令 fi 取得最小值的 j 的值(最优决策点)。若 p 单调不降,则称 f 具有决策单调性。
四边形不等式
以上述转移方程为例,若对于任意 a≤b≤c≤d 均满足:
w(a,c)+w(b,d)≤w(a,d)+w(b,c)
则称 w 满足四边形不等式,简记为交叉优于包含。
证明时可以采用另一个形式:对于任意 i≤j 满足 w(i,j)+w(i+1,j+1)≤w(i,j+1)+w(i+1,j) 。
若 w 满足四边形不等式,则上述转移方程满足决策单调性。
证明:因为 a→d,b→c 不优,而 a→c,b→d 更优,于是有决策单调性。
四边形不等式的另一个形式为:若对于任意 i≤j 均满足:
w(i,j)+w(i+1,j+1)≤w(i,j+1)+w(i+1,j)
则称 w 满足四边形不等式。
区间包含单调性
以上述转移方程为例,若对于任意 a≤b≤c≤d 均满足: w(a,d)≥w(b,c) ,则称 w 满足区间包含单调性。
单峰性转移的指针优化
对于某个状态的最优转移点,其左边的贡献向左单调递减,右边的贡献向右单调递减,则转移具有单峰性。若转移还满足决策单调性,则可以做到均摊 O(1) 转移。
考虑记录一个指针 p 表示决策点,由于决策单调性,p 不会往回跳。遍历每个位置时判断 p 的最优性,如果后面更优,则 p 向后跳,否则停住并转移。
根据单峰性这样子一定不会漏最优解。一般的决策单调性若不满足单峰性,使用该流程会局限在一个局部最优解中。
有 n 个数,可以进行两种操作:
- 选择一个 i ,令 ai←ai−1 ,花费 t 的代价。
- 对于所有 i ,令 ai←max(ai−1,0) ,代价为 s+k×r ,其中 r 为操作前 ai=0 的 i 的数量。
q 次询问,每次给出一个 h ,求所有 ai≤h 的最小操作代价。
n,t,s,k,ai,q,hi≤105
若固定了全局操作的次数,则最优决策一定是先全局后单点,由此可以预处理前缀和后 O(1) 算出 calc(h,x) 表示询问 h 时使用 x 次全局操作的答案。
注意到对于单个 h ,calc 是关于 x 的单峰性函数,且 h 增加时最优决策 x 单调不升,故可以使用上面的流程做到线性。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 1e5 + 7; |
| |
| ll f[N], sum[N], ans[N]; |
| int a[N], cnt[N]; |
| |
| int n, t, s, k, q; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| signed main() { |
| n = read(), t = read(), s = read(), k = read(); |
| |
| for (int i = 1; i <= n; ++i) |
| ++cnt[a[i] = read()]; |
| |
| int m = *max_element(a + 1, a + n + 1); |
| |
| for (int i = 1, num = cnt[0]; i <= m; num += cnt[i++]) |
| f[i] = f[i - 1] + 1ll * num * k + s; |
| |
| for (int i = m; ~i; --i) |
| sum[i] = sum[i + 1] + 1ll * cnt[i] * i, cnt[i] += cnt[i + 1]; |
| |
| auto calc = [](int h, int x) { |
| return f[x] + (sum[x + h] - 1ll * cnt[x + h] * (x + h)) * t; |
| }; |
| |
| for (int i = 0, j = m; i <= m; ++i) { |
| while (j && calc(i, j - 1) < calc(i, j)) |
| --j; |
| |
| ans[i] = calc(i, j); |
| } |
| |
| q = read(); |
| |
| while (q--) |
| printf("%lld ", ans[read()]); |
| |
| return 0; |
| } |
二分队列优化
使用条件:向后枚举时,某种决策只会被更后的决策反超。
形式化的,对于任意两个决策 j1<j2 ,存在一个 x 满足 i≤x 时 j1 优于 j2 ,i>x 时 j1 劣于 j2 。
可以证明基于四边形不等式的转移方程同样满足该条件。
实现
建立一个队列维护决策点,队列中保存若干三元组 (j,l,r) ,表示最优决策点为 j 的区间为 [l,r] 。
遍历枚举 i ,执行以下操作:
- 检查队头:设队头为 (j0,l0,r0) ,若 r0=i−1 ,则删除队头;否则令 l0←i 。
- 取队头保存最优决策点 j 进行转移求出 fi 。
- 尝试插入新决策 i ,步骤如下:
- 取出队尾,记为 (jt,lt,rt) 。
- 若对于 lt 来说, i 决策优于 jt 决策,记 pos=lt ,删除队尾重新执行上一步。
- 否则若对于 rt 来说,i 决策优于 jt 决策,则在 [lt,rt] 上二分查找位置 pos ,满足 [pos,n] 的最优决策点均为 i 。
- 将三元组 (i,pos,n) 插入队尾。
时间复杂度 O(nlogn) 。
应用
列出 DP 式:
fi=min(fj+|(si−sj)+(i−j)−(L+1)|P)
按上述方法优化即可。
| #include <bits/stdc++.h> |
| typedef long double ld; |
| using namespace std; |
| const int N = 1e5 + 7, S = 3e1 + 7; |
| |
| struct Node { |
| int j, l, r; |
| } q[N]; |
| |
| ld f[N]; |
| int s[N], g[N]; |
| bool ed[N]; |
| char str[N][S]; |
| |
| int n, L, P; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = c == '-'; |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= c == '-'; |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| inline ld mi(ld a, int b) { |
| ld res = 1; |
| |
| for (; b; b >>= 1, a *= a) |
| if (b & 1) |
| res *= a; |
| |
| return res; |
| } |
| |
| inline ld calc(int i, int j) { |
| return f[j] + mi(abs(s[i] - s[j] - L), P); |
| } |
| |
| signed main() { |
| int T = read(); |
| |
| while (T--) { |
| n = read(), L = read() + 1, P = read(); |
| |
| for (int i = 1; i <= n; ++i) { |
| scanf("%s", str[i]); |
| s[i] = s[i - 1] + strlen(str[i]) + 1; |
| } |
| |
| int head = 1, tail = 0; |
| q[++tail] = (Node) {0, 1, n}; |
| |
| for (int i = 1; i <= n; ++i) { |
| if (q[head].r == i - 1) |
| ++head; |
| |
| f[i] = calc(i, g[i] = q[head].j); |
| |
| if (++q[head].l > q[head].r) |
| ++head; |
| |
| while (head <= tail && calc(q[tail].l, i) <= calc(q[tail].l, q[tail].j)) |
| --tail; |
| |
| if (head > tail) |
| q[++tail] = (Node) {i, i + 1, n}; |
| else { |
| auto search = [](int l, int r, int i, int j) { |
| int pos = r + 1; |
| |
| while (l <= r) { |
| int mid = (l + r) >> 1; |
| |
| if (calc(mid, i) <= calc(mid, j)) |
| pos = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| return pos; |
| }; |
| |
| int pos = search(q[tail].l, q[tail].r, i, q[tail].j); |
| |
| if (pos <= n) |
| q[tail].r = pos - 1, q[++tail] = (Node) {i, pos, n}; |
| } |
| } |
| |
| if (f[n] > 1e18) |
| puts("Too hard to arrange"); |
| else { |
| printf("%.0LF\n", f[n]); |
| fill(ed + 1, ed + 1 + n, false); |
| |
| for (int i = n; i; i = g[i]) |
| ed[i] = true; |
| |
| for (int i = 1; i <= n; ++i) |
| printf("%s%c", str[i], " \n"[ed[i]]); |
| } |
| |
| puts("--------------------"); |
| } |
| |
| return 0; |
| } |
给出 a1∼n ,对于每个 i∈[1,n] ,求一个最小的非负整数 p ,使得对于所有 j∈[1,n] 都有 aj≤ai+p−√|i−j| 。
n≤5×105
转化限制条件为:
p+ai≥nmaxj=1{aj+√|i−j|}
将绝对值拆开,正反各做一次,得到:
p+ai≥i−1maxj=1{aj+√i−j}
记 w(j,i)=√i−j ,则 w(j,i) 满足四边形不等式,于是 fi=maxi−1j=1{aj+√i−j} 满足决策单调性,直接上二分队列可以做到 O(nlogn) 。
事实上本题的反超点是可以 O(1) 计算的。设两个决策点 k<j ,反超点为 i ,则:
ak+√i−k<aj+√i−j√i−k−√i−j<aj−ak(1)(2)
当 aj−ak≤0 时不等式恒不成立,令 d=aj−ak>0 ,化简得到:
√i−k<d+√i−ji−k<d2+i−j+2d√i−jj−k−d2<2d√i−j(3)(4)(5)
当 j−k−d2≤0 时上式恒成立,否则两边平方得到 i>j+(j−k−d2)24d2 ,于是得到反超点为 j+⌊(j−k−d2)24d2⌋+1 。
时间复杂度 O(n) (忽略预处理开根号的时间)。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 5e5 + 7; |
| |
| struct Node { |
| int j, l, r; |
| } q[N]; |
| |
| double sq[N], f[N]; |
| int a[N]; |
| |
| int n; |
| |
| inline double calc(int i, int j) { |
| return a[j] + sq[i - j]; |
| } |
| |
| inline void solve() { |
| int head = 1, tail = 0; |
| q[++tail] = (Node) {1, 2, n}; |
| |
| for (int i = 2; i <= n; ++i) { |
| if (q[head].r == i - 1) |
| ++head; |
| |
| f[i] = max(f[i], calc(i, q[head].j)); |
| |
| if (++q[head].l > q[head].r) |
| ++head; |
| |
| while (head <= tail && calc(q[tail].l, i) >= calc(q[tail].l, q[tail].j)) |
| --tail; |
| |
| if (head > tail) |
| q[++tail] = (Node) {i, i + 1, n}; |
| else { |
| ll d = a[i] - a[q[tail].j], |
| k = i - q[tail].j - d * d, |
| pos = (d <= 0 ? q[tail].r + 1 : i + ceil((double)k * k / (d * d * 4))); |
| |
| if (pos <= n) |
| q[tail].r = pos - 1, q[++tail] = (Node) {i, pos, n}; |
| } |
| } |
| } |
| |
| signed main() { |
| scanf("%d", &n); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i), f[i] = a[i], sq[i] = sqrt(i); |
| |
| solve(); |
| reverse(a + 1, a + n + 1), reverse(f + 1, f + n + 1); |
| solve(); |
| reverse(a + 1, a + n + 1), reverse(f + 1, f + n + 1); |
| |
| for (int i = 1; i <= n; ++i) |
| printf("%d\n", (int)ceil(f[i] - a[i])); |
| |
| return 0; |
| } |
二分栈优化
使用条件:向后枚举时,某种决策只会被更前的决策反超。
形式化的,对于任意两个决策 j1<j2 ,存在一个 x 满足 i≤x 时 j1 劣于 j2 ,i>x 时 j1 优于 j2 。
实现
用单调栈维护所有有用的决策,其中栈顶是当前最优决策。
每次加入一个点 i 时,记栈顶决策点为 j ,栈顶下面的决策点为 k ,若 (j,k) 的反超点不超过 (i,j) 的反超点,则说明 j 会先被 k 反超而没机会反超 i ,将其弹出。然后再一个个弹出已经被反超的决策即可。
计算完 fi 后,考虑求决策点为 i 的后缀。由于决策单调性,所以可以二分。
具体地,每次将 i 与栈顶的决策比较,若栈顶的决策区间内 i 恒优则弹栈,否则求出分界点后修改栈顶决策区间并压入 i 及相关后缀。
应用
将一个数列分成若干段,从每一段中选定一个数 s0 ,假设这个数有 t 个,则这一段价值为 s0t2 。求每一段的价值和的最大值。
n≤105
可以发现最优方案一定满足两端都是 s0 ,否则可以缩小区间获得更大值。
设 fi 表示以 i 结尾的最大价值和,则:
fi=imaxj=1{fj−1+si×(sumi−sumj+1)2}
固定 j 时,si×(sumi−sumj+1)2 单调递增。故对于一个 j1<j2 ,存在一个分界点满足分界点前 j2 更优,分界点后 j1 更优。用二分栈优化即可做到 O(nlogn) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 1e5 + 7; |
| |
| vector<int> vec[N]; |
| |
| ll f[N]; |
| int a[N], buc[N], s[N]; |
| |
| int n; |
| |
| inline ll calc(int j, int t) { |
| return f[j - 1] + 1ll * a[j] * t * t; |
| } |
| |
| inline int check(int x, int y) { |
| int l = max(s[x], s[y]), r = n, pos = n + 1; |
| |
| while (l <= r) { |
| int mid = (l + r) >> 1; |
| |
| if (calc(x, mid - s[x] + 1) >= calc(y, mid - s[y] + 1)) |
| pos = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| return pos; |
| } |
| |
| signed main() { |
| scanf("%d", &n); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i), s[i] = ++buc[a[i]]; |
| |
| for (int i = 1; i <= n; ++i) { |
| vector<int> &sta = vec[a[i]]; |
| #define tp1 sta.back() |
| #define tp2 sta[sta.size() - 2] |
| |
| while (sta.size() >= 2 && check(tp2, tp1) <= check(tp1, i)) |
| sta.pop_back(); |
| |
| sta.emplace_back(i); |
| |
| while (sta.size() >= 2 && check(tp2, tp1) <= s[i]) |
| sta.pop_back(); |
| |
| f[i] = calc(tp1, s[i] - s[tp1] + 1); |
| #undef tp1 |
| #undef tp2 |
| } |
| |
| printf("%lld", f[n]); |
| return 0; |
| } |
整体二分优化
某些 DP 形式如下(相邻层之间转移):
fi,j=mink≤j(fi−1,k+w(k,j)) (1≤i≤n,1≤j≤m)
其中 i∈[1,n] ,j∈[1,m] ,共 n×m 个状态,每个状态有 O(m) 个决策,暴力做时间复杂度 O(nm2) 。
令 mi,j 为,最优决策点若 ∀i,j,mi,j≤mi,j+1 ,则可以运用分治思想,递归得到 m 的上下界,就可以达到 O(nmlogm) 的时间复杂度。
当 w(l,r) 不好直接求,而使用莫队可以推出时,整体二分的结构可以保证移动指针的总复杂度为 O(nmlogn) 。
分治时相邻两次 mid 的改变量加起来应与 R−L 同级,故右端点移动次数为 O(nlogn) 。左端点一定从上一次分治的某个端点移动而来,在此次分治内移动次数与 r−l 同级,也是 O(nlogn) 。
有 n 个人,需要将他们分成 k 组,每组内人的编号连续。给出一个陌生值矩阵,定义一组的陌生值为每一对人的陌生值之和,求总陌生值的最小值。
n≤4000 ,k≤min(n,800) ,0≤ai,j≤9
记 w(l,r) 表示将 l∼r 的人分为一组的代价,即矩阵中左上角为 (l,l) 右下角为 (r,r) 的元素和的一半。则 w 满足决策单调性,使用整体二分优化可以做到 O(nklogn) 。
| #include <bits/stdc++.h> |
| using namespace std; |
| const int inf = 0x3f3f3f3f; |
| const int N = 4e3 + 7, K = 8e2 + 7; |
| |
| int f[K][N]; |
| int a[N][N]; |
| |
| int n, k; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| inline int w(int l, int r) { |
| return a[r][r] - a[l - 1][r] - a[r][l - 1] + a[l - 1][l - 1]; |
| } |
| |
| void solve(int d, int l, int r, int L, int R) { |
| if (L > R) |
| return; |
| |
| int mid = (L + R) >> 1, p = 0; |
| f[d][mid] = inf; |
| |
| for (int i = l; i <= min(mid, r); ++i) { |
| int res = f[d - 1][i - 1] + w(i, mid); |
| |
| if (res <= f[d][mid]) |
| f[d][mid] = res, p = i; |
| } |
| |
| solve(d, l, p, L, mid - 1), solve(d, p, r, mid + 1, R); |
| } |
| |
| signed main() { |
| n = read(), k = read(); |
| |
| for (int i = 1; i <= n; ++i) |
| for (int j = 1; j <= n; ++j) |
| a[i][j] = read(); |
| |
| for (int i = 1; i <= n; ++i) |
| for (int j = 1; j <= n; ++j) |
| a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1]; |
| |
| memset(f[0], inf, sizeof(f[0])); |
| f[0][0] = 0; |
| |
| for (int i = 1; i <= k; ++i) |
| solve(i, 1, n, 1, n); |
| |
| printf("%d", f[k][n] / 2); |
| return 0; |
| } |
给定一个序列 a1∼n ,要把它分成 k 个子段。每个子段的费用是其中相同元素的对数,求所有子段的费用之和的最小值。
n≤105 ,k≤min(n,20)
设 fi,j 表示前 i 个元素分为 j 段的最小费用,w(l,r) 表示 [l,r] 的费用,则:
fi,j=max(fk−1,j−1+w(k,i))
不难发现 w(l,r) 满足四边形不等式,故 f 每一层的转移都具有决策单调性。
令 solve(l, r, L, R)
表示 fi,L∼R 的决策点在 [l,r] 中,则可以每次计算出 fi,mid 的决策点,将序列分为两部分分治。
接下来考虑如何计算 fi,mid 的决策点,直接计算是困难的,可以类似莫队维护一个指针即可。
总时间复杂度 O(nklogn) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const ll inf = 0x3f3f3f3f3f3f3f3f; |
| const int N = 1e5 + 7, K = 2e1 + 7; |
| |
| ll f[K][N]; |
| int a[N]; |
| |
| int n, k; |
| |
| namespace MoAlgorithm { |
| int cnt[N]; |
| |
| ll result; |
| int l = 1, r; |
| |
| inline void update(int x, int k) { |
| result -= 1ll * cnt[x] * (cnt[x] - 1) / 2; |
| cnt[x] += k; |
| result += 1ll * cnt[x] * (cnt[x] - 1) / 2; |
| } |
| |
| inline ll query(int L, int R) { |
| while (l > L) |
| update(a[--l], 1); |
| |
| while (r < R) |
| update(a[++r], 1); |
| |
| while (l < L) |
| update(a[l++], -1); |
| |
| while (r > R) |
| update(a[r--], -1); |
| |
| return result; |
| } |
| } |
| |
| void solve(int l, int r, int L, int R, int d) { |
| if (L > R) |
| return; |
| |
| int mid = (L + R) >> 1, p = l; |
| f[d][mid] = f[d - 1][l - 1] + MoAlgorithm::query(l, mid); |
| |
| for (int i = l + 1; i <= min(mid, r); ++i) { |
| ll res = f[d - 1][i - 1] + MoAlgorithm::query(i, mid); |
| |
| if (res < f[d][mid]) |
| f[d][mid] = res, p = i; |
| } |
| |
| solve(l, p, L, mid - 1, d), solve(p, r, mid + 1, R, d); |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &k); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| memset(f, inf, sizeof(f)); |
| f[0][0] = 0; |
| |
| for (int i = 1; i <= k; ++i) |
| solve(1, n, 1, n, i); |
| |
| printf("%lld", f[k][n]); |
| return 0; |
| } |
给定排列 p1∼n ,将 p 划分成 k 段,使每一段的顺序对个数和最小。
n≤2.5×104,k≤25
设 fi,j 表示前 i 个数分为 j 段的答案,w(l,r) 表示 pl∼r 的顺序对个数,则:
fi,j=i−1mink=0{fk,j−1+w(k+1,i)}
注意到这里的 w(l,r) 并不好求,于是考虑采用整体二分维护决策单调性配合莫队+树状数组求顺序对优化即可,时间复杂度 O(nklog2n) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const ll inf = 1e18; |
| const int N = 2.5e4 + 7, K = 27; |
| |
| ll f[N][K]; |
| int a[N]; |
| |
| int n, k; |
| |
| namespace MoAlgorithm { |
| namespace BIT { |
| int c[N]; |
| |
| inline void update(int x, int k) { |
| for (; x <= n; x += x & -x) |
| c[x] += k; |
| } |
| |
| inline int query(int x) { |
| int res = 0; |
| |
| for (; x; x -= x & -x) |
| res += c[x]; |
| |
| return res; |
| } |
| } |
| |
| ll result; |
| int l = 1, r = 0; |
| |
| inline ll calc(int ql, int qr) { |
| while (l > ql) { |
| --l; |
| BIT::update(a[l], 1); |
| result += (r - l + 1) - BIT::query(a[l]); |
| } |
| |
| while (r < qr) { |
| ++r; |
| result += BIT::query(a[r]); |
| BIT::update(a[r], 1); |
| } |
| |
| while (l < ql) { |
| result -= (r - l + 1) - BIT::query(a[l]); |
| BIT::update(a[l], -1); |
| l++; |
| } |
| |
| while (r > qr) { |
| BIT::update(a[r], -1); |
| result -= BIT::query(a[r]); |
| r--; |
| } |
| |
| return result; |
| } |
| } |
| |
| void solve(int l, int r, int L, int R, const int d) { |
| if (L > R) |
| return; |
| |
| int mid = (L + R) >> 1, mnpos = 0; |
| f[mid][d] = inf; |
| |
| for (int i = l; i <= min(mid, r); ++i) { |
| ll res = f[i - 1][d - 1] + MoAlgorithm::calc(i, mid); |
| |
| if (res < f[mid][d]) |
| f[mid][d] = res, pos = i; |
| } |
| |
| solve(l, pos, L, mid - 1, d), solve(pos, r, mid + 1, R, d); |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &k); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| for (int i = 1; i <= n; ++i) |
| f[i][0] = inf; |
| |
| for (int i = 1; i <= k; ++i) |
| solve(1, n, 1, n, i); |
| |
| printf("%lld", f[n][k]); |
| return 0; |
| } |
给定 a1∼n 和 b1∼n ,对于所有 k∈[1,n] ,求从 1∼n 中选 k 个数,求 (∑a)−(maxb) 最大值。
n≤2×105
先考虑只有一个 k 的情况,这是一个经典问题,按 b 升序排序后枚举 maxb ,则 ∑a 只要求前 k 大就行了。
设 f(k,i) 表示 k 在 i 处决策( maxb=bi )的答案。若对于 j<i 有 f(k,j)<f(k,i) ,那么 k→k+1 时,由于 i 所选的 a 一定不小于 j 所选的 a ,故 f(k+1,j)<f(k+1,i) ,因此 f(k) 具有决策单调性。
用整体二分维护决策,主席树查询前 k 大即可做到 O(nlognlogV) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const ll inf = 1e18; |
| const int N = 2e5 + 7; |
| |
| struct Node { |
| ll a, b; |
| |
| inline bool operator < (const Node &rhs) const { |
| return b < rhs.b; |
| } |
| } nd[N]; |
| |
| ll f[N]; |
| |
| int n; |
| |
| namespace SMT { |
| const int S = 3e7 + 7; |
| |
| ll s[S]; |
| int lc[S], rc[S], cnt[S]; |
| int rt[N]; |
| |
| int tot; |
| |
| int update(int x, int nl, int nr, int pos) { |
| int y = ++tot; |
| lc[y] = lc[x], rc[y] = rc[x], cnt[y] = cnt[x] + 1, s[y] = s[x] + pos; |
| |
| if (nl == nr) |
| return y; |
| |
| int mid = (nl + nr) >> 1; |
| |
| if (pos <= mid) |
| lc[y] = update(lc[x], nl, mid, pos); |
| else |
| rc[y] = update(rc[x], mid + 1, nr, pos); |
| |
| return y; |
| } |
| |
| ll query(int x, int nl, int nr, int k) { |
| if (nl == nr) |
| return 1ll * nl * k; |
| |
| int mid = (nl + nr) >> 1; |
| return k > cnt[rc[x]] ? s[rc[x]] + query(lc[x], nl, mid, k - cnt[rc[x]]) : query(rc[x], mid + 1, nr, k); |
| } |
| } |
| |
| void solve(int L, int R, int l, int r) { |
| if (L > R) |
| return; |
| |
| if (l == r) { |
| for (int i = L; i <= R; ++i) |
| f[i] = SMT::query(SMT::rt[l], -1e9, 1e9, i) - nd[l].b; |
| |
| return; |
| } |
| |
| int mid = (L + R) >> 1, pos = 0; |
| f[mid] = -inf; |
| |
| for (int i = max(mid, l); i <= r; ++i) { |
| ll res = SMT::query(SMT::rt[i], -1e9, 1e9, mid) - nd[i].b; |
| |
| if (res > f[mid]) |
| f[mid] = res, pos = i; |
| } |
| |
| solve(L, mid - 1, l, pos), solve(mid + 1, R, pos, r); |
| } |
| |
| signed main() { |
| scanf("%d", &n); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%lld%lld", &nd[i].a, &nd[i].b); |
| |
| sort(nd + 1, nd + n + 1); |
| |
| for (int i = 1; i <= n; ++i) |
| SMT::rt[i] = SMT::update(SMT::rt[i - 1], -1e9, 1e9, nd[i].a); |
| |
| solve(1, n, 1, n); |
| |
| for (int i = 1; i <= n; ++i) |
| printf("%lld\n", f[i]); |
| |
| return 0; |
| } |
从 n 个蛋糕中选择 m 个,每个蛋糕有 v,c 两个属性,记选的蛋糕编号为 k1∼m,最大化 ∑mi=1vki−∑mi=1|cki−cki+1| ,其中 km+1=k1 。
n≤2×105
先考虑最小化 ∑mi=1|cki−cki+1| ,显然这个式子在将 c 排序后取得最小值 2((maxmi=1cki)−(minmi=1cki)) 。
设 fl,r 表示 [l,r] 的答案,即区间中前 m 大的 v 减去 2(cr−cl) ,可以发现随 r 增加,取得最大值的 l 具有决策单调性。剩下的就和上题差不多了。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const ll inf = 1e18; |
| const int N = 2e5 + 7; |
| |
| struct Node { |
| int v, c; |
| |
| inline bool operator < (const Node &rhs) const { |
| return c < rhs.c; |
| } |
| } nd[N]; |
| |
| ll f[N]; |
| |
| int n, m; |
| |
| namespace SMT { |
| const int S = 3e7 + 7; |
| |
| ll s[S]; |
| int lc[S], rc[S], cnt[S]; |
| int rt[N]; |
| |
| int tot; |
| |
| int update(int x, int nl, int nr, int pos) { |
| int y = ++tot; |
| lc[y] = lc[x], rc[y] = rc[x], cnt[y] = cnt[x] + 1, s[y] = s[x] + pos; |
| |
| if (nl == nr) |
| return y; |
| |
| int mid = (nl + nr) >> 1; |
| |
| if (pos <= mid) |
| lc[y] = update(lc[x], nl, mid, pos); |
| else |
| rc[y] = update(rc[x], mid + 1, nr, pos); |
| |
| return y; |
| } |
| |
| ll query(int x, int y, int nl, int nr, int k) { |
| if (nl == nr) |
| return 1ll * nl * k; |
| |
| int mid = (nl + nr) >> 1, c = cnt[rc[y]] - cnt[rc[x]]; |
| return k > c ? s[rc[y]] - s[rc[x]] + query(lc[x], lc[y], nl, mid, k - c) : |
| query(rc[x], rc[y], mid + 1, nr, k); |
| } |
| } |
| |
| inline ll calc(int i, int j) { |
| return SMT::query(SMT::rt[j - 1], SMT::rt[i], 1, 1e9, m) + nd[j].c * 2 - nd[i].c * 2; |
| } |
| |
| void solve(int L, int R, int l, int r) { |
| if (L > R) |
| return; |
| |
| if (l == r) { |
| for (int i = L; i <= R; ++i) |
| f[i] = calc(i, l); |
| |
| return; |
| } |
| |
| int mid = (L + R) >> 1, pos = 0; |
| f[mid] = -inf; |
| |
| for (int i = l; i <= min(r, mid - m + 1); ++i) { |
| ll res = calc(mid, i); |
| |
| if (res > f[mid]) |
| f[mid] = res, pos = i; |
| } |
| |
| solve(L, mid - 1, l, pos), solve(mid + 1, R, pos, r); |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &m); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d%d", &nd[i].v, &nd[i].c); |
| |
| sort(nd + 1, nd + n + 1); |
| |
| for (int i = 1; i <= n; ++i) |
| SMT::rt[i] = SMT::update(SMT::rt[i - 1], 1, 1e9, nd[i].v); |
| |
| solve(m, n, 1, n - m + 1); |
| printf("%lld", *max_element(f + m, f + n + 1)); |
| return 0; |
| } |
有 n 个城市,最开始在城市 s 。在 d 天内,每天可以选择移动到相邻的城市或参观所处城市。第一次参观城市 i 时会获得 ai 的奖励,求最大奖励和。
n≤105
可以发现最优情况一定是一直向一个方向走或先向一个方向走一段后再反方向走一段。
考虑固定了走的区间,设剩余天数为 k ,那么肯定选前 k 大的 a 的和作为答案最优,可以用主席树实现。
先处理掉只往一个方向走的情况,考虑处理先往右后往左的情况,先左后右是类似的。注意到左端点变大时,最优的右端点一定是不降的,于是可以用整体二分优化做到 O(nlognlogV) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 1e5 + 7; |
| |
| int a[N], b[N]; |
| |
| ll ans; |
| int n, s, d, m; |
| |
| namespace SMT { |
| const int S = N << 5; |
| |
| ll s[S]; |
| int lc[S], rc[S], cnt[S]; |
| int rt[N]; |
| |
| int tot; |
| |
| int insert(int x, int nl, int nr, int p) { |
| int y = ++tot; |
| lc[y] = lc[x], rc[y] = rc[x], cnt[y] = cnt[x] + 1, s[y] = s[x] + b[p]; |
| |
| if (nl == nr) |
| return y; |
| |
| int mid = (nl + nr) >> 1; |
| |
| if (p <= mid) |
| lc[y] = insert(lc[x], nl, mid, p); |
| else |
| rc[y] = insert(rc[x], mid + 1, nr, p); |
| |
| return y; |
| } |
| |
| ll query(int x, int y, int nl, int nr, int k) { |
| if (k >= cnt[y] - cnt[x]) |
| return s[y] - s[x]; |
| |
| if (nl == nr) |
| return b[nl] * k; |
| |
| int mid = (nl + nr) >> 1; |
| return k <= cnt[rc[y]] - cnt[rc[x]] ? query(rc[x], rc[y], mid + 1, nr, k) : |
| s[rc[y]] - s[rc[x]] + query(lc[x], lc[y], nl, mid, k - (cnt[rc[y]] - cnt[rc[x]])); |
| } |
| } |
| |
| inline ll calc(int l, int r) { |
| int k = d - ((r - s) + (r - l)); |
| return k > 0 ? SMT::query(SMT::rt[l - 1], SMT::rt[r], 1, m, k) : 0; |
| } |
| |
| void solve(int l, int r, int L, int R) { |
| if (L > R) |
| return; |
| |
| if (l == r) { |
| for (int i = L; i <= R; ++i) |
| ans = max(ans, calc(i, l)); |
| |
| return; |
| } |
| |
| int mid = (L + R) >> 1, p = l; |
| ll res = calc(mid, l); |
| |
| for (int i = l + 1; i <= r; ++i) { |
| ll now = calc(mid, i); |
| |
| if (now > res) |
| res = now, p = i; |
| } |
| |
| ans = max(ans, res); |
| solve(l, p, L, mid - 1), solve(p, r, mid + 1, R); |
| } |
| |
| inline void solve() { |
| SMT::tot = 0; |
| |
| for (int i = 1; i <= n; ++i) |
| SMT::rt[i] = SMT::insert(SMT::rt[i - 1], 1, m, a[i]); |
| |
| for (int i = 1; i <= s; ++i) |
| ans = max(ans, calc(i, s)); |
| |
| solve(s + 1, n, 1, s - 1); |
| } |
| |
| signed main() { |
| scanf("%d%d%d", &n, &s, &d), ++s; |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i), b[i] = a[i]; |
| |
| sort(b + 1, b + n + 1), m = unique(b + 1, b + n + 1) - b - 1; |
| |
| for (int i = 1; i <= n; ++i) |
| a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b; |
| |
| solve(); |
| s = n - s + 1, reverse(a + 1, a + n + 1); |
| solve(); |
| printf("%lld", ans); |
| return 0; |
| } |
Knuth's Optimization
通常被用于区间合并问题,即将 n 个长度为 1 的区间合并起来,每次合并会有代价,求最优代价。
设 pl,r 为 fl,r 的最优决策点 k ,则 Knuth's Optimization 的使用条件为: ∀i<j,pl,r−1≤pl,r≤pl+1,r 。
若代价函数满足四边形不等式与区间包含单调性,可以证明转移方程满足 Knuth's Optimization 的使用条件。
实现
枚举 [l,r] 的决策时,只要枚举 [pl,r−1,pl+1,r] 中的决策即可,时间复杂度 O(n2) 。
应用
有 n 个村庄,放 m 个邮局,求每个村庄到最近邮局的距离和的最小值。
n≤3000 ,m≤300
设 fi,j 表示前 i 个村庄放 j 个邮局的最小距离和,w(l,r) 表示在 [l,r] 范围村庄放一个邮局的最小距离和,则有:
fi,j=i−1mink=0{fk,j−1+w(k+1,i)}
可以证明 w(l,r) 满足四边形不等式和区间包含单调性,于是可以决策单调性优化做到 O(n2) 。
| #include <bits/stdc++.h> |
| using namespace std; |
| const int inf = 0x3f3f3f3f; |
| const int N = 3e3 + 7, M = 3e2 + 7; |
| |
| int w[N][N], f[N][M], g[N][M]; |
| int a[N]; |
| |
| int n, m; |
| |
| signed main() { |
| scanf("%d%d", &n, &m); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| sort(a + 1, a + 1 + n); |
| |
| for (int l = 1; l <= n; ++l) |
| for (int r = l + 1; r <= n; ++r) |
| w[l][r] = w[l][r - 1] + (a[r] - a[(l + r) >> 1]); |
| |
| memset(f, inf, sizeof(f)); |
| f[0][0] = 0; |
| |
| for (int j = 1; j <= m; ++j) { |
| g[n + 1][j] = n; |
| |
| for (int i = n; i; --i) |
| for (int k = g[i][j - 1]; k <= g[i + 1][j]; ++k) |
| if (f[k][j - 1] + w[k + 1][i] <= f[i][j]) |
| f[i][j] = f[k][j - 1] + w[k + 1][i], g[i][j] = k; |
| } |
| |
| printf("%d", f[n][m]); |
| return 0; |
| } |
斜率优化
对于形如 fi=min{ai×bj+cj+di} 的 DP 方程,将其写作:
−ai×bj+fi−di=cj
把 (bj,cj) 看作一个点,每次就是用斜率为 −ai 的直线去切所有的决策判断哪个更优,即最小化截距 fi−di 。显然在该情形下(取 min ),最优决策点一定在点集的下凸壳上,于是设法维护凸壳即可。
单调队列优化
使用条件:b 单增、a 单减,即加入的点的 x 坐标、查询的斜率 −ai 均单增。
若两个决策点 j<k 满足 k 优于 j ,则:
ai×bk+ck+di≤ai×bj+cj+di−ai≥ck−cjbk−bj(6)(7)
记 slope(j,k)=ck−cjbk−bj ,则 −ai≥slope(j,k) 说明决策 k 优于 j ( k>j )。
考虑将决策点用单调队列存储,可以发现这个斜率很符合单调队列的性质:
- slope(qhead,qhead+1)≤−ai :因为 qhead 在 qhead+1 前加入,那么这个式子就表示 qhead 的决策不如 qhead+1 ,将队首弹出。
- slope(qtail−1,qtail)≥slope(qtail,i) :假设后面存在一个 at 使得 −at≥slope(qtail−1,qtail) ,由于 −ai 不降,等到 qtail−1 弹出后,qtail 也会被弹出,可以直接弹出 qtail 。
时间复杂度 O(n) ,按照题目具体分析维护严格/非严格凸壳(是否存在共线情况)。
| int head = 1, tail = 0; |
| q[++tail] = 0; |
| |
| for (int i = 1; i <= n; ++i) { |
| while (head < tail && slope(q[head], q[head + 1]) <= -A(i)) |
| ++head; |
| |
| f[i] = calc(i, q[head]); |
| |
| while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) |
| --tail; |
| |
| q[++tail] = i; |
| } |
有 n 个玩具,每个玩具有个长度 ci ,定义一段玩具 [l,r] 放入一个容器的长度为 x=r−l+∑ri=lci ,费用为 (x−L)2 ,其中 L 为常数,最小化将所有玩具放入容器的费用和。
n≤5×104
设 si=∑ij=1(ci+1) ,记 fi 为考虑前 i 个玩具的答案,则:
fi=min{fj+(si−sj−1−L)2}
为方便,令 L←L+1 ,则:
fi=min{−2sisj+(fj+(sj+L)2)+(s2i−2siL)}
可以发现这个 sj 是单增的,−2si 是单减的,于是直接上单调队列维护斜率优化即可做到 O(n) 。
| #include <bits/stdc++.h> |
| typedef long double ld; |
| typedef long long ll; |
| using namespace std; |
| const int N = 5e4 + 7; |
| |
| ll s[N], f[N]; |
| int q[N]; |
| |
| int n, L; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| inline ll calc(int i, int j) { |
| return f[j] + (s[i] - s[j] - L) * (s[i] - s[j] - L); |
| } |
| |
| inline ld slope(int i, int j) { |
| return (ld)(f[i] + (s[i] + L) * (s[i] + L) - f[j] - (s[j] + L) * (s[j] + L)) / (s[i] - s[j]); |
| } |
| |
| signed main() { |
| n = read(), L = read() + 1; |
| |
| for (int i = 1; i <= n; ++i) |
| s[i] = s[i - 1] + read() + 1; |
| |
| int head = 1, tail = 0; |
| q[++tail] = 0; |
| |
| for (int i = 1; i <= n; ++i) { |
| while (head < tail && slope(q[head], q[head + 1]) <= 2 * s[i]) |
| ++head; |
| |
| f[i] = calc(i, q[head]); |
| |
| while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) |
| --tail; |
| |
| q[++tail] = i; |
| } |
| |
| printf("%lld", f[n]); |
| return 0; |
| } |
给定 a1∼n ,将其分为 m 段,最小化 m2s2 ,其中 s2 表示方差。
n≤3000
先推式子:
m2s2=m×m∑i=1(xi−¯¯¯x)2=(mm∑i=1x2i)−(m∑i=1xi)2(8)(9)
于是问题转化为最小化 ∑mi=1x2i ,发现 m 越大答案越小( (a+b)2>a2+b2 ),猜测它是下凸的,直接上 wqs 二分,斜率优化部分不难,时间复杂度 O(nlogV) 。
| #include <bits/stdc++.h> |
| typedef long double ld; |
| typedef long long ll; |
| using namespace std; |
| const int N = 3e3 + 7; |
| |
| ll s[N], f[N]; |
| int a[N], q[N], g[N]; |
| |
| int n, m; |
| |
| inline ld slope(int i, int j) { |
| return (ld) (f[j] + s[j] * s[j] - f[i] - s[i] * s[i]) / (s[j] - s[i]); |
| } |
| |
| inline void calc(int lambda) { |
| int head = 1, tail = 0; |
| q[++tail] = 0; |
| |
| for (int i = 1; i <= n; ++i) { |
| while (head < tail && slope(q[head], q[head + 1]) <= 2 * s[i]) |
| ++head; |
| |
| auto calc = [&](int i, int j) { |
| return f[j] + (s[i] - s[j]) * (s[i] - s[j]) - lambda; |
| }; |
| |
| f[i] = calc(i, q[head]), g[i] = g[q[head]] + 1; |
| |
| while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) |
| --tail; |
| |
| q[++tail] = i; |
| } |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &m); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i), s[i] = s[i - 1] + a[i]; |
| |
| int l = -1e9, r = 0, res = 0; |
| |
| while (l <= r) { |
| int mid = (l + r) >> 1; |
| calc(mid); |
| |
| if (g[n] >= m) |
| res = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| calc(res), f[n] += 1ll * res * m; |
| printf("%lld", f[n] * m - s[n] * s[n]); |
| return 0; |
| } |
二分队列优化
使用条件:加入点的 x 坐标 b 单增。
用单调队列维护凸壳,询问时在凸壳上二分斜率 −ai 即可。
有 n 个任务按顺序分批执行,每批任务开始需要一个固定的启动时间 S 。第 i 个任务花费的时间是 ti ,每个任务的花费是它完成的时刻乘上它自身的费用系数 ci。需要找到一个最佳的分批顺序使得总费用最小。
n≤3×105 ,1≤s≤28 ,|Ti|≤28 ,0≤ci≤28
记 Ti,Ci 为前缀和数组,设 fi 表示把前 i 个任务分成若干个组的最小花费,有转移方程:
fi=min{fj+Ti×(Ci−Cj)+S×(Cn−Cj)}
把这个式子整理一下:
fi=min{−Ti×Cj+(fj−S×Cj)+(Ti×Ci+S×Cn)}
可以发现加入的点的 x 坐标单增,于是用二分队列优化即可做到 O(nlogn) ,需要注意避免小数运算丢精度。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 3e5 + 7; |
| |
| ll f[N]; |
| int t[N], c[N], q[N]; |
| |
| int n, s; |
| |
| inline int X(int i) { |
| return c[i]; |
| } |
| |
| inline ll Y(int i) { |
| return f[i] - 1ll * s * c[i]; |
| } |
| |
| inline double slope(int i, int j) { |
| return (double) ((f[j] - 1ll * s * c[j]) - (f[i] - 1ll * s * c[i])) / (c[j] - c[i]); |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &s); |
| |
| for (int i = 1; i <= n; ++i) { |
| scanf("%d%d", t + i, c + i); |
| t[i] += t[i - 1], c[i] += c[i - 1]; |
| } |
| |
| int head = 1, tail = 0; |
| q[++tail] = 0; |
| |
| for (int i = 1; i <= n; ++i) { |
| auto search = [&](int k) { |
| int l = head, r = tail - 1, pos = tail; |
| |
| while (l <= r) { |
| int mid = (l + r) >> 1; |
| |
| if (Y(q[mid + 1]) - Y(q[mid]) >= 1ll * k * (X(q[mid + 1]) - X(q[mid]))) |
| pos = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| return q[pos]; |
| }; |
| |
| auto calc = [](int i, int j) { |
| return f[j] + 1ll * t[i] * (c[i] - c[j]) + 1ll * s * (c[n] - c[j]); |
| }; |
| |
| f[i] = calc(i, search(t[i])); |
| |
| auto cmp = [](int i, int j, int k) { |
| return (Y(j) - Y(i)) * (X(k) - X(j)) >= (Y(k) - Y(j)) * (X(j) - X(i)); |
| }; |
| |
| while (head < tail && cmp(q[tail - 1], q[tail], i)) |
| --tail; |
| |
| q[++tail] = i; |
| } |
| |
| printf("%lld", f[n]); |
| return 0; |
| } |
cdq分治优化
设函数 cdq(l, r)
表示求解 fl∼r ,返回 l∼r 点的凸壳。
先递归处理 [l,mid] 与 [mid+1,r] 。对于当前层,枚举 [mid+1,r] 的点,计算 [l,mid] 点的凸壳所产生的贡献。计算完答案后将左右两部分凸壳归并返回。
可以做到 O(nlogn) 的复杂度。
有 n 天,初始有 S 元。每天可以:
- 花一些现金买入股票,其中第 i 天股票中 A 股和 B 股的数量比为 ri ,即只能按 ri 的比值买入股票。
- 或按相同比例卖出 A 股和 B 股,并按当天的价值获得现金,其中第 i 天 A 股价值 ai 元、B 股价值 bi 元。
- 或什么也不干。
同一天内可以进行多次操作,求 n 天后能够获得的最大价值。
n≤105
可以发现必然存在一种最优的买卖方案满足:每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。
设第 i 天最大收益为 fi ,并设 fi 可以换为两种股票数量分别为 xi=firiairi+bi,yi=fi1airi+bi 。
枚举上一次买入的时间 j ,可以得到转移方程:
fi=max(fi−1,maxj<i{xjai+yjbi})
前一项是好处理的,考虑后一项,设转移到 fi 时 j 优于 k ,则:
xjai+yjbi>xkai+ykbiyj−ykxj−xk>−aibi
很明显 x,y 都是没有单调性的,无法直接用单调栈或队列建凸壳。
考虑按 −aibi cdq 分治,分治时对前一块的 x 排序,就可以建出凸壳优化转移,时间复杂度 O(nlogn) 。
| #include <bits/stdc++.h> |
| using namespace std; |
| const double inf = 1e9; |
| const int N = 1e5 + 7; |
| |
| struct Node { |
| double x, y, k; |
| int id; |
| } nd[N], tmp[N]; |
| |
| double a[N], b[N], rate[N], f[N]; |
| int sta[N]; |
| |
| double S; |
| int n; |
| |
| inline double slope(int a, int b) { |
| return nd[a].x == nd[b].x ? inf : (nd[b].y - nd[a].y) / (nd[b].x - nd[a].x); |
| } |
| |
| void cdq(int l, int r) { |
| if (l == r) { |
| f[l] = max(f[l], f[l - 1]); |
| nd[l].x = f[l] * rate[l] / (a[l] * rate[l] + b[l]); |
| nd[l].y = f[l] / (a[l] * rate[l] + b[l]); |
| return; |
| } |
| |
| int mid = (l + r) >> 1, lp = l, rp = mid + 1; |
| |
| for (int i = l; i <= r; ++i) { |
| if (nd[i].id <= mid) |
| tmp[lp++] = nd[i]; |
| else |
| tmp[rp++] = nd[i]; |
| } |
| |
| memcpy(nd + l, tmp + l, sizeof(Node) * (r - l + 1)); |
| cdq(l, mid); |
| int top = 0; |
| |
| for (int i = l; i <= mid; ++i) { |
| while (top > 1 && slope(sta[top], i) > slope(sta[top - 1], sta[top])) |
| --top; |
| |
| sta[++top] = i; |
| } |
| |
| for (int i = mid + 1, j = 1; i <= r; ++i) { |
| while (j < top && slope(sta[j], sta[j + 1]) > nd[i].k) |
| ++j; |
| |
| f[nd[i].id] = max(f[nd[i].id], nd[sta[j]].x * a[nd[i].id] + nd[sta[j]].y * b[nd[i].id]); |
| } |
| |
| cdq(mid + 1, r); |
| inplace_merge(nd + l, nd + mid + 1, nd + r + 1, [](const Node &a, const Node &b) { return a.x < b.x; }); |
| } |
| |
| signed main() { |
| scanf("%d%lf", &n, &S); |
| |
| for (int i = 1; i <= n; ++i) { |
| scanf("%lf%lf%lf", a + i, b + i, rate + i); |
| nd[i].x = S * rate[i] / (a[i] * rate[i] + b[i]); |
| nd[i].y = S / (a[i] * rate[i] + b[i]); |
| nd[i].k = -a[i] / b[i]; |
| nd[i].id = i, f[i] = S; |
| } |
| |
| sort(nd + 1, nd + n + 1, [](const Node &a, const Node &b) { return a.k > b.k; }); |
| cdq(1, n); |
| printf("%.3lf", f[n]); |
| return 0; |
| } |
李超树优化
这是一个很无脑的做法,用李超树来直接维护一次函数(直线),时间复杂度可以做到 O(nlogn) 。
有 n 个柱子,高度为 h1∼n 。若一座桥连接了 i,j ,付出 (hi−hj)2 的代价。未被桥连接的柱子将会被拆除,付出 wi 的代价。求通过桥将 1,n 两根柱子连接的最小代价,桥不能在端点以外的任何地方相交。
n≤105
首先设 si 表示 w 的前缀和,fi 表示联通 1,i 的代价,则:
fi=h2i+si−1+i−1minj=1{−2hihj+fj+h2j−sj}
用李超树维护直线 y=−2hjx+(fj+h2j−sj) ,每次查询 x=hi 时 y 的最小值,时间复杂度 O(nlogn) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const ll inf = 1e18; |
| const int N = 1e5 + 7, V = 1e6 + 7; |
| |
| struct Line { |
| ll k, b; |
| |
| inline ll operator () (const int x) { |
| return k * x + b; |
| } |
| }; |
| |
| ll s[N], f[N]; |
| int h[N], w[N]; |
| |
| int n; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| namespace SMT { |
| Line s[V << 2]; |
| |
| inline int ls(int x) { |
| return x << 1; |
| } |
| |
| inline int rs(int x) { |
| return x << 1 | 1; |
| } |
| |
| void maintain(int x, int nl, int nr, Line k) { |
| int mid = (nl + nr) >> 1; |
| |
| if (k(mid) < s[x](mid)) |
| swap(k, s[x]); |
| |
| if (nl == nr) |
| return; |
| |
| if (k(nl) < s[x](nl)) |
| maintain(ls(x), nl, mid, k); |
| |
| if (k(nr) < s[x](nr)) |
| maintain(rs(x), mid + 1, nr, k); |
| } |
| |
| ll query(int x, int nl, int nr, int pos) { |
| if (nl == nr) |
| return s[x](pos); |
| |
| int mid = (nl + nr) >> 1; |
| |
| if (pos <= mid) |
| return min(s[x](pos), query(ls(x), nl, mid, pos)); |
| else |
| return min(s[x](pos), query(rs(x), mid + 1, nr, pos)); |
| } |
| } |
| |
| signed main() { |
| n = read(); |
| |
| for (int i = 1; i <= n; ++i) |
| h[i] = read(); |
| |
| for (int i = 1; i <= n; ++i) |
| s[i] = s[i - 1] + (w[i] = read()); |
| |
| int maxh = *max_element(h + 1, h + 1 + n); |
| fill(SMT::s + 1, SMT::s + 1 + 4 * maxh, (Line) {0, inf}); |
| SMT::maintain(1, 1, maxh, (Line) {-2ll * h[1], f[1] + 1ll * h[1] * h[1] - s[1]}); |
| |
| for (int i = 2; i <= n; ++i) { |
| f[i] = 1ll * h[i] * h[i] + s[i - 1] + SMT::query(1, 1, maxh, h[i]); |
| SMT::maintain(1, 1, maxh, (Line) {-2ll * h[i], f[i] + 1ll * h[i] * h[i] - s[i]}); |
| } |
| |
| printf("%lld", f[n]); |
| return 0; |
| } |
应用
给出 n 个区间,将它们分为两份满足两份区间不交,可以丢弃区间。
第一问:求两份区间数量较小者的最大值。
第二问:对于所有 i∈[1,n] ,求强制选取第 i 个区间时的第一问。
n≤200
首先将值域离散化为 [1,m] 。设 cnt(l,r) 表示被 [l,r] 完全包含的区间个数,不难 O(n3) 预处理得到。
再设 fi,j 表示值域以 i 结尾的前缀选 j 个区间到第一份,此时第二份最多能选区间的数量。不难得到转移方程:
fi,j=i−1maxk=1{fk,j−cnt(k+1,i),fk,j+cnt(k+1,i)}
答案即为:
nmaxi=0{min(fm,i,i)}
于是第一问可以 O(n3) 解决。
对于第二问,设 gi,j 表示值域以 i 开始的后缀选择 j 个区间到第一份,此时第二份最多能选区间的数量,转移与 f 类似。
由对称性,钦定强制选取的区间 [l,r] 给第一份,于是答案为:
l−1maxi=1mmaxj=r+1nmaxk=0nmaxt=0min(k+t+cnt(i+1,j−1),fi,k+gj,t)
直接做是 O(n4) 的,考虑优化。设:
h(l,r)=nmaxi=0nmaxj=0min(i+j+cnt(l+1,r−1),fl,i+gr,j)
则答案即为 maxl−1i=1maxmj=r+1h(l,r) ,这一部分可以做到 O(n2) 。
接下来考虑如何计算 h(l,r) 。可以发现 min 里面的东西是两份的区间数,由对称性,令 k+t+cnt(l+1,r−1) 为较大值,则需要最大化 fl,i+gr,j 。注意到 fl,i 随 i 的增大而减小,gr,j 随 j 的增大而减小。那么当 i 增加时再增加 j 显然不优,因此可以单纯增加左边、减少右边。这一部分时间复杂度降为 O(n3) 。
总时间复杂度 O(n3) 。
| #include <bits/stdc++.h> |
| using namespace std; |
| const int inf = 0x3f3f3f3f; |
| const int N = 2e2 + 7, M = 4e2 + 7; |
| |
| struct Interval { |
| int l, r; |
| } a[N]; |
| |
| int cnt[M][M], f[M][N], g[M][N], h[M][M]; |
| |
| int n, m; |
| |
| signed main() { |
| scanf("%d", &n); |
| vector<int> vec = {-inf, inf}; |
| |
| for (int i = 1; i <= n; ++i) { |
| scanf("%d%d", &a[i].l, &a[i].r); |
| a[i].r += a[i].l - 1; |
| vec.emplace_back(a[i].l), vec.emplace_back(a[i].r); |
| } |
| |
| sort(vec.begin(), vec.end()); |
| vec.erase(unique(vec.begin(), vec.end()), vec.end()); |
| |
| for (int i = 1; i <= n; ++i) { |
| a[i].l = lower_bound(vec.begin(), vec.end(), a[i].l) - vec.begin() + 1; |
| a[i].r = lower_bound(vec.begin(), vec.end(), a[i].r) - vec.begin() + 1; |
| } |
| |
| m = vec.size(); |
| |
| for (int i = 1; i <= n; ++i) |
| for (int j = 1; j <= a[i].l; ++j) |
| for (int k = a[i].r; k <= m; ++k) |
| ++cnt[j][k]; |
| |
| memset(f[0] + 1, -inf, sizeof(int) * n); |
| |
| for (int i = 1; i <= m; ++i) |
| for (int j = 0; j <= n; ++j) { |
| f[i][j] = -inf; |
| |
| for (int k = 0; k < i; ++k) { |
| if (f[k][j] != -inf) |
| f[i][j] = max(f[i][j], f[k][j] + cnt[k + 1][i]); |
| |
| if (f[k][max(j - cnt[k + 1][i], 0)] != -inf) |
| f[i][j] = max(f[i][j], f[k][max(j - cnt[k + 1][i], 0)]); |
| } |
| } |
| |
| int ans = 0; |
| |
| for (int i = 0; i <= n; ++i) |
| ans = max(ans, min(f[m][i], i)); |
| |
| printf("%d\n", ans); |
| memset(g[m + 1] + 1, -inf, sizeof(int) * n); |
| |
| for (int i = m; i; --i) |
| for (int j = 0; j <= n; ++j) { |
| g[i][j] = -inf; |
| |
| for (int k = i + 1; k <= m + 1; ++k) { |
| if (g[k][j] != -inf) |
| g[i][j] = max(g[i][j], g[k][j] + cnt[i][k - 1]); |
| |
| if (g[k][max(j - cnt[i][k - 1], 0)] != -inf) |
| g[i][j] = max(g[i][j], g[k][max(j - cnt[i][k - 1], 0)]); |
| } |
| } |
| |
| for (int i = 1; i <= m; ++i) |
| for (int j = i + 2; j <= m; ++j) { |
| h[i][j] = -inf; |
| |
| for (int k = 0, t = n; k <= n && f[i][k] != -inf; ++k) { |
| while (~t && g[j][t] != -inf) { |
| if (min(f[i][k] + g[j][t], k + t + cnt[i + 1][j - 1]) >= h[i][j]) |
| h[i][j] = min(f[i][k] + g[j][t], k + t + cnt[i + 1][j - 1]); |
| else |
| break; |
| |
| --t; |
| } |
| |
| ++t; |
| } |
| } |
| |
| for (int i = 1; i <= n; ++i) { |
| int ans = 0; |
| |
| for (int j = 1; j < a[i].l; ++j) |
| for (int k = a[i].r + 1; k <= m; ++k) |
| ans = max(ans, h[j][k]); |
| |
| printf("%d\n", ans); |
| } |
| |
| return 0; |
| } |
给定 a1∼n ,求其一个子序列 b1∼m ,最大化 ∑mi=1ibi 。
n≤105 ,|ai|≤107
设 fi,j 表示考虑了前 i 个数,选了 j 个数的答案,则:
fi,j=max(fi−1,j,fi−1,j−1+j×ai)
直接做是 O(n2) 的。
根据 wqs 二分那套理论,这个 fi 关于 j 的函数具有凸性,然后修改是全局加一个一次函数后与原来的值取 max 。
由于斜率递减,因此存在一个分界点 p ,使得:
- 对于 j≤p ,有 fi,j=fi−1,j 。
- 对于 j>p ,有 fi,j=fi−1,j−1+j×ai 。
那么每次二分找到这个分界点,然后就是区间加上一个一次函数。
考虑要维护的操作:插入一个 fi,i ,区间加一个一次函数。不难用 fhq-Treap 维护,时间复杂度 O(nlogn) 。
| #include <bits/stdc++.h> |
| typedef unsigned int uint; |
| typedef long long ll; |
| using namespace std; |
| const int N = 1e6 + 7; |
| |
| int a[N]; |
| |
| int n; |
| |
| namespace fhqTreap { |
| struct Tag { |
| ll a, d; |
| } tag[N]; |
| |
| ll val[N], rval[N]; |
| uint dat[N]; |
| int lc[N], rc[N], siz[N]; |
| |
| mt19937 myrand(time(0)); |
| int root, tot; |
| |
| inline int newnode(ll k) { |
| dat[++tot] = myrand(), val[tot] = rval[tot] = k, siz[tot] = 1; |
| return tot; |
| } |
| |
| inline void pushup(int x) { |
| siz[x] = siz[lc[x]] + siz[rc[x]] + 1; |
| rval[x] = rc[x] ? rval[rc[x]] : val[x]; |
| } |
| |
| inline void spread(int x, Tag k) { |
| val[x] += k.a + (siz[lc[x]] + 1) * k.d; |
| rval[x] += k.a + siz[x] * k.d; |
| tag[x].a += k.a, tag[x].d += k.d; |
| } |
| |
| inline void pushdown(int x) { |
| spread(lc[x], tag[x]); |
| tag[x].a += 1ll * (siz[lc[x]] + 1) * tag[x].d; |
| spread(rc[x], tag[x]); |
| tag[x] = (Tag) {0, 0}; |
| } |
| |
| void split_siz(int x, int k, int &a, int &b) { |
| if (!x) { |
| a = b = 0; |
| return; |
| } |
| |
| pushdown(x); |
| |
| if (k <= siz[lc[x]]) |
| b = x, split_siz(lc[x], k, a, lc[x]); |
| else |
| a = x, split_siz(rc[x], k - siz[lc[x]] - 1, rc[x], b); |
| |
| pushup(x); |
| } |
| |
| int merge(int a, int b) { |
| if (!a || !b) |
| return a | b; |
| |
| if (dat[a] > dat[b]) |
| return pushdown(a), rc[a] = merge(rc[a], b), pushup(a), a; |
| else |
| return pushdown(b), lc[b] = merge(a, lc[b]), pushup(b), b; |
| } |
| |
| pair<int, ll> search(int id, int x, int k, ll v) { |
| if (!x) |
| return make_pair(0, 0); |
| |
| pushdown(x); |
| int rk = k + siz[lc[x]] + 1; |
| |
| if (val[x] >= (lc[x] ? rval[lc[x]] : v) + 1ll * rk * a[id]) { |
| auto res = search(id, rc[x], rk, val[x]); |
| return res == make_pair(0, 0ll) ? make_pair(rk, val[x]) : res; |
| } else |
| return search(id, lc[x], k, v); |
| } |
| |
| inline void insert(int pos, ll v) { |
| int x = 0; |
| split_siz(root, pos, root, x); |
| root = merge(merge(root, newnode(v)), x); |
| } |
| |
| inline void update(int pos, Tag k) { |
| int x = 0; |
| split_siz(root, pos, root, x); |
| spread(x,k); |
| root = merge(root, x); |
| } |
| |
| ll dfs(int x) { |
| ll ans = val[x]; |
| pushdown(x); |
| |
| if (lc[x]) |
| ans = max(ans, dfs(lc[x])); |
| |
| if (rc[x]) |
| ans = max(ans, dfs(rc[x])); |
| |
| return ans; |
| } |
| } |
| |
| signed main() { |
| scanf("%d", &n); |
| |
| for (int i = 1; i <= n; ++i) { |
| scanf("%d", a + i); |
| auto res = fhqTreap::search(i, fhqTreap::root, 0, 0); |
| fhqTreap::insert(res.first, res.second); |
| fhqTreap::update(res.first, {1ll * res.first * a[i], a[i]}); |
| } |
| |
| printf("%lld", max(fhqTreap::dfs(fhqTreap::root), 0ll)); |
| return 0; |
| } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步