Dyd's Blog

He who has a strong enough why can bear almost any how.

优化dp4

然而我 dp 废的一比

优化dp4

才发现这个还没写……

上一篇:优化dp3

wqs 二分

(并不保证真确性的)定义

以下全是自己的理解:感觉就是用来求一个“选 $m$ 个,最优化”的问题,更具体的,设选 $x$ 个的最大权值为 $f(x)$ ,wqs 二分要求 $f(x)-x$ 图像是这样(就是先随着可选个数增多变优,后来因为必须要选 $x$ 个导致变坏),形式化的,其实是要求它是个凸函数:f(x)-x

如图,设绿色直线为 $y = kx + b$ ,由切点 $D(x_D, f(x_D))$ 得 $b = f(x_D) - kx_D$ ,考虑到这是切线,显然对于一个确定的 $k$ , $x_D$ 是使 $b$ 最大的点,那么可以二分一个 $k$ ,把每个物品的价值 $-k$ (如果是代价就 $+k$ ),然后做没有个数限制的 dp 求出最大的价值和就是 $b$ ,此时对应的 $x$ (如绿线对应的 $x_D$ )若比 $m$ 大,说明 $k$ 应继续变大;若比 $m$ 小,$k$ 应该变小;若相等,则用 $k, x/m$ 计算得到 $f(m)$

值得一提的是,很多时候如果 $x, f(x)$ 都是整数,那么 $k$ 也可以只在整数范围二分:考虑求导,有 $f’(x) = k$ ,大部分情况下 $f(x)$ 的导函数 $f’(x)$ 都是“比较好看的”,加上 $x$ 是整数,那么 $k$ 一半也是整数

但是,整数又会遇到一个问题:如果一条切线过了几个点(包括 $m$ )怎么办(更具体的,当斜率为 $k$ 时 $x = m - 1$ ,斜率变化 $1$ 时 $x = m + 1$ )?其实此时我们不也是算出了 $m$ 的斜率吗,直接就用此时的 $k$ 就好了(优先用小的那个算,即 $x = m - 1$ 那个)

带权二分的好处在于,对于要求“选则恰好 $k$ 个“,我们 dp 时几乎不可避免的要给 $k$ 一维,这让状态数(和时间复杂度)直接乘了个 $n$ (个数),而 wqs 二分可以避免加一维,把 $n$ 优化至 $\log v$ (权值)

另外,一个细节是,向下取整( l + r >> 1 )和向 $0$ 取整( $(l + r) / 2$ )是不一样的;且 $l, r$ 的初始值是斜率最大值和最小值啊,如果 $f(x)$ 是上凸的,然后 $x$ 的范围是 $[0,n]$ ,那么 $r$ 应该等于 $f(1)−f(0)$ 的最大可能值,$l$ 同理等于 $f(n)−f(n−1)$ 的最小可能值

例题十二

经典题:Tree I

思路

二分一个 $k$ ,对于每条白边,把权值改为 $c - k$ ,直接做最小生成树即可,时间 $O(m \log m \log k \alpha(n))$ ( $k$ 是边权),可以黑白边分开排序,每次合并,时间 $O(m \alpha(n) \log k + m \log m)$

细节是一条切线过几个点的情况,此时明显白边权值与黑边权值相等,我们优先选白边,还有 $k$ 可以为负数

(所以这和 dp 有啥关系啊)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
#define fi first
#define se second
using PII = std::pair<int, int>;
const int N = 5e4+ 100, M = 1e5 + 100;
int n, m, num, ans, cta = 0, ctb = 0;
struct Edge{ int u, v, w, c; } e[M], a[M], b[M];
int fa[N], rk[N];
int get(int x){ return fa[x] == x ? x : fa[x] = get(fa[x]); }
void merge(int x, int y)
{
if (rk[x] > rk[y]) std::swap(x, y);
fa[x] = y, rk[y] += rk[x] == rk[y];
}
PII chk(int k)
{
int i = 1, j = 1, ct = 0, x, y;
PII res = {0, 0};
while (i <= cta && j <= ctb)
{
if (a[i].w - k < b[j].w) e[++ct] = a[i++], e[ct].w -= k;
else e[++ct] = b[j++];
}
while (i <= cta) e[++ct] = a[i++], e[ct].w -= k;
while (j <= ctb) e[++ct] = b[j++];
for (i = 1; i <= n; ++i) fa[i] = i, rk[i] = 1;
for (i = 1; i <= ct; ++i)
{
x = get(e[i].u), y = get(e[i].v);
if (x == y) continue;
res.fi += e[i].w;
res.se += e[i].c == 0;
merge(x, y);
}
return res;
}
int main()
{
scanf("%d %d %d", &n, &m, &num);
for (int i = 1; i <= m; ++i)
{
scanf("%d %d %d %d", &e[i].u, &e[i].v, &e[i].w, &e[i].c);
++e[i].u, ++e[i].v;
if (e[i].c == 0) a[++cta] = e[i];
else b[++ctb] = e[i];
}
std::sort(a + 1, a + cta + 1, [&](Edge x, Edge y){ return x.w < y.w; });
std::sort(b + 1, b + ctb + 1, [&](Edge x, Edge y){ return x.w < y.w; });
int l = -101, r = 101, mid;
PII t;
while (l <= r)
{
mid = (l + r) >> 1;
t = chk(mid);
if (t.se <= num)
{
ans = t.fi + mid * num;
l = mid + 1;
}
else r = mid - 1;
}
printf("%d\n", ans);
return 0;
}

例题十三

看看真正的 dp 题(不难发现大部分时候 dp 都是套在 wqs 二分内部的):林克卡特树

思路

为了方便记题目中的 $k$ 为 $num$ 显然切断 $num$ 条边后原树变成 $num + 1$ 个联通块(更进一步,它们都是树),我们求出每棵树的直径(最长链,可以是点),用 $num$ 条权为 $0$ 的边连起来就是最优解

考虑树形 dp ,套路的设 $d[i, j]$ 表示”点 $i$ 的子树中有 $j$ 条链的最大值”,然后发现没法转移,没法转移的关键问题是,当考虑合并 $u, v$ 两个点代表的联通块集合时,会出现两个链合并成一条链/链数不变/链数加 $1$ 三个情况,我们当前信息无法指导我们如何转移

考虑加一维,不难发现在最后选的链中每个点的度数只有 $0/1/2$ ,于是令 $d[i, j, k](k = 0/1/2)$ 表示”点 $i$ 的子树中有 $j$ 条链,且 $i$ 的度数为 $k$ 的最大值“,那么转移就分讨设 $v$ 为 $u$ 的儿子:

  1. 不选 $(u, v)$ : $d[u, j_1, k_1] + d[v, j_2, k_2] \to d[u, j_1 + j_2, k_1]$
  2. 选则 $(u, v)$ : $d[u, j_1, k_1] + d[v, j_2, k_2] \to d[u, j_1 + j_2 + dt, k_1 + 1]$ ,其中 $k_1, k_2 \ne 2$ , $dt = [k_1 == 0 \wedge k_2 == 0] - [k_1 == 1 \wedge k_2 == 1]$

那么边界显然就是 $d[i, 0, 0] = 0$ 其它都是 $-\infty$ ,吗?发现如果是以一个点作为链,那就是 $d[i, 1, ?] = 0$ ,但 $?$ (也就是这个点的度)该是多少呢?显然我们不能让这个点与其它任何点相连,所以是 $2$ ,即 $d[i, 1, 2] = d[i, 0, 0] = 0$ 其它都是 $-\infty$ ,于是 dp ,答案就是 $\max(d[root, num, 0/1/2])$ ,直接 dp 时间为 $O(n * num * 3)$ ,我们发现很难优化,因为转移已经是 $O(1)$ 了,时间瓶颈在状态数,现在我们要么减少计算的状态数(即只计算一部分状态),要么减少状态数(即修改状态的定义)

此时不难发现 这篇 blog 讲的是 wqs 二分 此题也是”选 $num$ 个“的形式,我们把 $num = 0 \sim 100$ 的答案依次求一下,发现这是一个凸函数(不会证,具体做题时靠感觉 + 打表 + 猜想),上 wqs 二分,把第二维直接去掉,转移时发现边数减少就加一个 $k$ ,否则减一个 $k$ (这个 $k$ 是二分的)

最后是 wqs 二分的经典问题:凸包上多点共线怎么解决;我们打个结构体 dp 时优先转移边数少的即可,注意去掉第二维后出现自己转移到自己的情况,用临时数组避免多次转移

代码

$l, r$ 初始值一定要够大,注意是 $num + 1$ 个联通块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <bits/stdc++.h>
using LL = long long;
const int N = 3e5 + 100, inf = 0x3f3f3f3f;
const LL INF = 1e18;
int n, num, l, r, mid, h[N], idx = 0;
struct Edge{ int ne, ver, w; } e[N << 1];
struct DP
{
LL v; int num;
bool operator < (DP x) const { return v == x.v ? num > x.num : v < x.v; }
DP operator + (DP x){ return {v + x.v, num + x.num}; }
} d[N][3], as, td[3];
LL ans;
void add(int x, int y, int z){ e[idx] = {h[x], y, z}, h[x] = idx++; }
template<typename T>
bool cmx(T &x, T y){ return x < y ? x = y, true : false; }
void dp(int x, int fa)
{
for (int i = h[x], y; ~i; i = e[i].ne) if ((y = e[i].ver) != fa)
{
dp(y, x);
for (int j = 0; j <= 2; ++j) td[j] = {-INF, inf};
for (int j = 0; j <= 2; ++j)
for (int k = 0; k <= 2; ++k) cmx(td[j], d[x][j] + d[y][k]);
cmx(td[1], d[x][0] + d[y][0] + DP{e[i].w - mid, 1});
cmx(td[1], d[x][0] + d[y][1] + DP{e[i].w, 0});
cmx(td[2], d[x][1] + d[y][0] + DP{e[i].w, 0});
cmx(td[2], d[x][1] + d[y][1] + DP{e[i].w + mid, -1});
for (int j = 0; j <= 2; ++j) d[x][j] = td[j];
}
}
int main()
{
scanf("%d %d", &n, &num);
memset(h + 1, -1, n << 2);
for (int i = 1, u, v, w; i < n; ++i)
{
scanf("%d %d %d", &u, &v, &w);
add(u, v, w), add(v, u, w);
}
l = -1e12, r = 1e12;
while (l <= r)
{
mid = (l + r) >> 1;
for (int i = 1; i <= n; ++i)
{
d[i][0] = {0, 0};
d[i][1] = {-INF, inf};
d[i][2] = {-mid, 1};
}
dp(1, 0);
as = d[1][0], cmx(as, d[1][1]), cmx(as, d[1][2]);
if (as.num <= num + 1)
{
ans = as.v + LL(mid) * (num + 1);
r = mid - 1;
}
else l = mid + 1;
}
printf("%lld\n", ans);
return 0;
}

未完待续?