动态规划
- 通过发掘转移时最优解的必要条件往往能使抽象的转移条件变得简单。
- 当问题有多维的限制时,将其中一维排序往往能简化状态设计和转移。
树形DP
典例
题目大意
- 给一棵 n n n 个点的带边权树,要求找出 k k k 个点 A 1 , A 2 , … , A k A_1, A_2, \dots, A_k A1,A2,…,Ak,使得 ∑ i = 1 k − 1 dist ( A i , A i + 1 ) \sum\limits_{i = 1}^{k - 1}\text{dist}(A_i,A_{i + 1}) i=1∑k−1dist(Ai,Ai+1) 最小。
- n , k ≤ 3000 n,k \le 3000 n,k≤3000
解法
- 设 f u , j f_{u,j} fu,j 表示从点 u u u 出发,在 u u u 的子树中经过 j j j 个点最后回到点 u u u 的最小距离和。
- 设 g u , j g_{u,j} gu,j 表示从点 u u u 出发,在 u u u 的子树中经过 j j j 个点最后停在任意一点(也相当于从 u u u 的子树中任意一点出发,经过 j j j 个点最后回到点 u u u)的最小距离和。
- 设 h u , j h_{u,j} hu,j 表示在 u u u 的子树中从任意一点出发,经过 j j j 个点并保证经过点 u u u,最后停在任意一点的最小距离和。
- 显然
f
,
g
,
h
f,g,h
f,g,h 都是由
u
u
u 的子节点
v
v
v 转移过来,则我们可以得到如下转移(为了描述方便,设已经处理完的
u
u
u 的子节点的子树结点集合为
A
A
A,当前处理的子节点
v
v
v 的子树结点集合为
B
B
B,用
→
\to
→ 表示每种转移所对应在树上走的方案,转移前令
f
′
=
f
,
g
′
=
g
,
h
′
=
h
f' = f, g' = g, h' = h
f′=f,g′=g,h′=h):
- 对于
f
u
,
j
f_{u,j}
fu,j
f u , j + k ′ = min { f v , k + f u , j + 2 dist ( u , v ) } u → A → u → B → u \begin{aligned} f'_{u,j+k} &= \min\{f_{v,k} + f_{u,j} + 2\text{dist}(u,v)\} & u \to A \to u \to B \to u \\ \end{aligned} fu,j+k′=min{fv,k+fu,j+2dist(u,v)}u→A→u→B→u - 对于
g
u
,
j
g_{u,j}
gu,j
g u , j + k ′ = min { g v , k + f u , j + dist ( u , v ) } u → A → u → B g u , j + k ′ = min { f v , k + g u , j + 2 dist ( u , v ) } u → B → u → A \begin{aligned} g'_{u, j + k} &= \min\{g_{v, k} + f_{u,j} + \text{dist}(u,v)\} & u \to A \to u \to B \\ g'_{u, j + k} &= \min\{f_{v, k} + g_{u,j} + 2\text{dist}(u,v)\} & u \to B \to u \to A \\ \end{aligned} gu,j+k′gu,j+k′=min{gv,k+fu,j+dist(u,v)}=min{fv,k+gu,j+2dist(u,v)}u→A→u→Bu→B→u→A - 对于
h
u
,
j
h_{u,j}
hu,j
h u , j + k ′ = min { f v , k + h u , j + 2 dist ( u , v ) } A → u → B → u → A h u , j + k ′ = min { h v , k + f u , j + 2 dist ( u , v ) } B → u → A → u → B h u , j + k ′ = min { g v , k + g u , j + dist ( u , v ) } A → u → B \begin{aligned} h'_{u, j + k} & = \min\{f_{v,k} + h_{u,j} + 2\text{dist}(u,v)\} & A \to u \to B \to u \to A \\ h'_{u, j + k} &= \min\{h_{v,k} + f_{u,j} + 2\text{dist}(u,v)\} & B \to u \to A \to u \to B \\ h'_{u, j + k} & = \min\{g_{v, k} + g_{u,j} + \text{dist}(u,v)\} & A \to u \to B \end{aligned} hu,j+k′hu,j+k′hu,j+k′=min{fv,k+hu,j+2dist(u,v)}=min{hv,k+fu,j+2dist(u,v)}=min{gv,k+gu,j+dist(u,v)}A→u→B→u→AB→u→A→u→BA→u→B
- 对于
f
u
,
j
f_{u,j}
fu,j
- 时间复杂度 O ( n 2 ) \mathcal O(n^2) O(n2)。
树上背包 DP
- 问题 给一棵树,要求设计一个动态规划算法,求出以每个点为根大小不超过 k k k 的连通块个数。
- 算法 设 f x , i f_{x,i} fx,i 表示以 x x x 为根大小为 i i i 的连通块个数,当合并一个子结点 y y y,采用如下的转移方式:
for (int i = 0; i <= sze[x] + sze[y]; ++i)
g[i] = 0;
for (int i = 0; i <= min(sze[x], k); ++i)
for (int j = 0; j <= min(sze[y], k); ++j)
g[i + j] += f[x][i] * f[y][j];
sze[x] += sze[y];
for (int i = 0; i <= sze[x]; ++i)
f[x][i] = g[i];
- 引理 当 k = n k = n k=n 时,该算法的时间复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2)。
证明 考虑由树上结点生成的 n 2 n^2 n2 个点对,将枚举次数看作是点对的选择,每个点对只会在其 LCA 处被恰好统计一次。
- 定理 该算法的复杂度为 O ( n k ) \mathcal O(nk) O(nk)。
证明 记合并过程 sze x ≤ k \text{sze}_x \le k szex≤k 则称此时的 x x x 为小子树,否则为大子树。考虑分几种情况讨论:
若大子树与大子树合并,显然大子树的个数不会超过 n k \dfrac{n}{k} kn 个,故总的合并复杂度不会超过 k 2 × n k = n k k^2 \times \dfrac{n}{k} = nk k2×kn=nk。
若小子树与大子树合并,小子树合并后必然成为大子树,因此合并的小子树必然互不相交,总合并复杂度不超过 n k nk nk。
若小子树与小子树合并,以整体的眼光来看,假设这些合并过程最终产生了大小为 x 1 , x 2 , … , x m x_1, x_2, \dots, x_m x1,x2,…,xm 的 m m m 棵子树,由 引理,分析最劣情况的复杂度可以看作是求解以下问题:
maximize ∑ i = 1 m x i 2 subject to x i ≤ k ∀ 1 ≤ i ≤ m ∑ i = 1 m x i = n \begin{aligned} \text{maximize} &\sum\limits_{i = 1}^{m}x_i^2 \\ \text{subject to} & \hspace{0.5em} x_i \le k & \forall 1 \le i \le m\\ &\sum\limits_{i = 1}^{m}x_i = n \\ \end{aligned} maximizesubject toi=1∑mxi2xi≤ki=1∑mxi=n∀1≤i≤m
容易通过调整法证明,将每个 x i x_i xi 取得尽可能接近 k k k 可使上式取得最大值,因此上限同样为 n k nk nk。
矩阵快速幂优化 DP
- 以下面的顺序计算矩阵乘法,访问内存连续,效率最高。
friend inline matrix operator * (const matrix &a, const matrix &b)
{
matrix c;
c.Clear(a.gn, b.gm);
for (int i = 0; i < a.gn; ++i)
for (int k = 0; k < a.gm; ++k)
{
int s = a.g[i][k];
for (int j = 0; j < b.gm; ++j)
c.g[i][j] = (1ll * s * b.g[k][j] + c.g[i][j]) % mod;
}
return c;
}
- 定义广义矩阵乘法 C i , j = ⨁ k = 1 n A i , k ⊗ B k , j C_{i,j} = \bigoplus\limits_{k=1}^{n} A_{i,k}\otimes B_{k,j} Ci,j=k=1⨁nAi,k⊗Bk,j只需满足 ⊗ \otimes ⊗ 具有结合律, ⊕ \oplus ⊕ 具有交换律,且 ⊗ \otimes ⊗ 对 ⊕ \oplus ⊕ 具有分配律, 矩阵乘法就存在结合律。
证明 假设有矩阵 D = A B C D = ABC D=ABC,需证明 D = A ( B C ) D = A(BC) D=A(BC),即
D i , j = ⨁ l = 1 n ( A B ) i , l ⊗ C l , j = ⨁ l = 1 n ( ⨁ k = 1 n A i , k ⊗ B k , l ) ⊗ C l , j = ⨁ l = 1 n ⨁ k = 1 n A i , k ⊗ B k , l ⊗ C l , j = ⨁ k = 1 n ⨁ l = 1 n A i , k ⊗ B k , l ⊗ C l , j = ⨁ k = 1 n ⨁ l = 1 n A i , k ⊗ ( B k , l ⊗ C l , j ) = ⨁ k = 1 n A i , k ⊗ ( ⨁ l = 1 n B k , l ⊗ C l , j ) = ⨁ k = 1 n A i , k ⊗ ( B C ) k , j \begin{aligned} D_{i,j} &= \bigoplus\limits_{l=1}^{n} (AB)_{i,l}\otimes C_{l,j} \\ &= \bigoplus\limits_{l=1}^{n} \left(\bigoplus\limits_{k=1}^{n} A_{i,k}\otimes B_{k,l}\right)\otimes C_{l,j} \\&= \bigoplus\limits_{l=1}^{n} \bigoplus\limits_{k=1}^{n} A_{i,k}\otimes B_{k,l}\otimes C_{l,j}\\&= \bigoplus\limits_{k=1}^{n} \bigoplus\limits_{l=1}^{n} A_{i,k}\otimes B_{k,l}\otimes C_{l,j}\\&= \bigoplus\limits_{k=1}^{n} \bigoplus\limits_{l=1}^{n} A_{i,k}\otimes \left(B_{k,l}\otimes C_{l,j}\right)\\&= \bigoplus\limits_{k=1}^{n} A_{i,k}\otimes \left(\bigoplus\limits_{l=1}^{n}B_{k,l}\otimes C_{l,j}\right)\\&= \bigoplus\limits_{k=1}^{n} A_{i,k}\otimes(BC)_{k,j} \end{aligned} Di,j=l=1⨁n(AB)i,l⊗Cl,j=l=1⨁n(k=1⨁nAi,k⊗Bk,l)⊗Cl,j=l=1⨁nk=1⨁nAi,k⊗Bk,l⊗Cl,j=k=1⨁nl=1⨁nAi,k⊗Bk,l⊗Cl,j=k=1⨁nl=1⨁nAi,k⊗(Bk,l⊗Cl,j)=k=1⨁nAi,k⊗(l=1⨁nBk,l⊗Cl,j)=k=1⨁nAi,k⊗(BC)k,j
单调队列优化 DP
- 单调队列/栈上结点也可以记录信息,如前缀和等,或在插入或删除的过程中用数据结构维护。
典例 POJ3017
- 设
f
i
f_i
fi 表示将前
i
i
i 个数分成若干段、满足每段所有数的和不超过
M
M
M 时各段最大值之和的最小值。不难得到转移:
f i = min 0 ≤ j < i 且 ∑ k = j + 1 i a k ≤ M { f j + max j + 1 ≤ k ≤ i { a k } } f_i = \min\limits_{0\le j < i且\sum\limits_{k =j+1}^{i}a_k\le M}\{f_j + \max\limits_{j + 1\le k\le i}\{a_k\}\} fi=0≤j<i且k=j+1∑iak≤Mmin{fj+j+1≤k≤imax{ak}} - 因为在此题的限制条件中
f
i
f_i
fi 是单调不降的,容易证明,最优解需满足以下两个条件之一:
- a j = max j ≤ k ≤ i { a k } a_j = \max\limits_{j\le k\le i}\{a_k\} aj=j≤k≤imax{ak}
- ∑ k = j i a k > M \sum \limits_{k = j}^{i}a_k > M k=j∑iak>M
- 第一个条件可以维护一个决策点 j j j 单调递增、数值 a j a_j aj 单调递减的队列,并用数据结构维护最优转移,这里使用的是懒惰删除的二叉堆,第二个条件只需要维护单个指针单独转移即可。
l = 0, ql = 1, qr = 0;
for (int i = 1; i <= n; ++i)
{
while (l < i && sum[i] - sum[l] > M)
++l;
while (ql <= qr && sum[i] - sum[que[ql]] > M)
val[que[ql++]] = -1;
while (ql <= qr && a[que[qr]] <= a[i])
val[que[--qr]] = -1;
if (ql <= qr)
q.push(point(val[que[qr]] = f[que[qr]] + a[i], que[qr]));
que[++qr] = i;
f[i] = f[l] + a[que[ql]];
while (!q.empty() && val[q.top().t] != q.top().s)
q.pop();
if (!q.empty())
CkMin(f[i], q.top().s);
}
多重背包
- 给定 n n n 种物品,其中第 i i i 种物品的体积为 v i v_i vi,价值为 w i w_i wi,有 c i c_i ci 个。
- 求能够放入容积 m m m 的背包的物品的最大价值总和。
二进制拆分法
- 求出 ∑ k = 0 p 2 k ≤ c i \sum \limits_{k = 0}^{p}2^k\le c_i k=0∑p2k≤ci 最大的 p p p,令 r i = c i − ∑ k = 0 p 2 k r_i = c_i - \sum\limits_{k = 0}^{p}2^k ri=ci−k=0∑p2k。
- 将数量为
c
i
c_i
ci 的第
i
i
i 种物品拆成
p
+
2
p + 2
p+2 种物品,其体积分别为:
2 0 v i , 2 1 v i , … , 2 p v i , r i v i 2^0v_i, 2^1v_i,\dots,2^pv_i,r_iv_i 20vi,21vi,…,2pvi,rivi - 时间复杂度 O ( n m log c ) \mathcal O(nm\log c) O(nmlogc),其中 c = max 1 ≤ i ≤ n { c i } c = \max\limits_{1\le i\le n}\{c_i\} c=1≤i≤nmax{ci}。
单调队列优化
- 将容积那一维按照模 v i v_i vi 的余数分类,则可利用单调队列优化。
- 时间复杂度 O ( n m ) \mathcal O(nm) O(nm)。
for (int i = 1; i <= n; ++i)
for (int j = 0; j < v[i]; ++j)
{
cm = 0;
for (int k = j; k <= m; k += v[i])
cur[++cm] = k;
int r = cm - 1, ql = 1, qr = 0;
for (int k = cm; k >= 2; --k)
{
while (ql <= qr && que[ql] >= k)
++ql;
while (r && k - r <= c[i])
{
while (ql <= qr && f[cur[que[qr]]] - que[qr] * w[i]
<= f[cur[r]] - r * w[i]) --qr;
que[++qr] = r--;
}
CkMax(f[cur[k]], f[cur[que[ql]]] + (k - que[ql]) * w[i]);
}
}
斜率优化 DP
- 形如下式的转移宜采用斜率优化:
f i = max / min { f j + A ( i ) B ( j ) + C ( i ) + D ( j ) } f_i = \max/\min\{f_j + A(i)B(j) + C(i)+D(j)\} fi=max/min{fj+A(i)B(j)+C(i)+D(j)}
其中 A , B , C , D A,B,C,D A,B,C,D 分别为含对应变量的多项式, A , B A,B A,B 中的最高次数为一次。 - 去除
max
/
min
\max/\min
max/min 的限制,移项得到:
− A ( i ) B ( j ) + f i − C ( i ) = f j + D ( j ) -A(i)B(j) + f_i - C(i) = f_j + D(j) −A(i)B(j)+fi−C(i)=fj+D(j)
将每个可能的决策 ( B ( j ) , f j + D ( j ) ) (B(j), f_j + D(j)) (B(j),fj+D(j)) 视作平面上的一点,最优决策即用固定斜率 − A ( i ) -A(i) −A(i) 的直线去截这些点,最大化/最小化截距,因而只需要维护这些点的上凸壳/下凸壳。 - 维护的方式视具体情况而定:
- 若 A ( i ) , B ( j ) A(i),B(j) A(i),B(j) 均单调,用单调队列/单调栈维护即可。
- 若 A ( i ) , B ( j ) A(i), B(j) A(i),B(j) 其中一个单调,将单调的那个变量视作决策点(若只有 A ( i ) A(i) A(i) 单调则需要倒着转移),求最优决策时在凸壳上二分即可。
- 若 A ( i ) , B ( j ) A(i),B(j) A(i),B(j) 均不单调,则需要用平衡树维护凸壳或离线后 CDQ \text{CDQ} CDQ分治。
- 尽量不要用实数判斜率,同时要注意横坐标相同的情况要特殊处理,视题目的要求只保留纵坐标最大/最小的点即可。
- 以下为平衡树
Splay
\text{Splay}
Splay 维护下凸壳的模板,利用了平衡树上二分和
Splay
操作,减少了部分常数。
namespace Hull
{
const int N = 1e5 + 5;
int rt;
int fa[N], lc[N], rc[N];
int suf[N], pre[N];
ll valx[N], valy[N];
inline void Init(int x, ll vx, ll vy)
{
valx[x] = vx;
valy[x] = vy;
}
inline bool Slope1(int x, int y, int z)
{
if (!x || !y || !z)
return true;
return (valy[y] - valy[x]) * (valx[z] - valx[y])
<= (valy[z] - valy[y]) * (valx[y] - valx[x]);
}
inline bool Slope2(int x, int y, ll z)
{
if (!x || !y)
return true;
return (valy[y] - valy[x]) <= z * (valx[y] - valx[x]);
}
inline void Rotate(int x)
{
int y = fa[x], z = fa[y];
bool flag = lc[y] == x;
int b = flag ? rc[x] : lc[x];
fa[x] = z, fa[y] = x;
b ? fa[b] = y : 0;
z ? (lc[z] == y ? lc[z] : rc[z]) = x : 0;
flag ? (rc[x] = y, lc[y] = b) : (lc[x] = y, rc[y] = b);
}
inline bool whichSide(int x)
{
return rc[fa[x]] == x;
}
inline void Splay(int x, int tar)
{
while (fa[x] != tar)
{
if (fa[fa[x]] != tar)
Rotate(whichSide(fa[x]) == whichSide(x) ? fa[x] : x);
Rotate(x);
}
!tar ? rt = x : 0;
}
inline int findLeft(int x, int y)
{
int res = x;
while (x)
{
if (Slope1(pre[x], x, y))
res = x, x = rc[x];
else
x = lc[x];
}
return res;
}
inline int findRight(int x, int y)
{
int res = x;
while (x)
{
if (Slope1(y, x, suf[x]))
res = x, x = lc[x];
else
x = rc[x];
}
return res;
}
inline void Clear(int &x)
{
if (!x)
return ;
Clear(lc[x]);
Clear(rc[x]);
fa[x] = pre[x] = suf[x] = 0;
x = 0;
}
inline void Insert(int id)
{
int x = rt, y = 0, dir;
while (x)
{
y = x;
if (valx[id] < valx[x])
x = lc[x], dir = 0;
else
x = rc[x], dir = 1;
}
fa[x = id] = y;
if (y) (dir ? rc[y] : lc[y]) = x;
Splay(x, 0);
if (lc[x])
{
int z = findLeft(lc[x], x);
Splay(z, x);
Clear(rc[z]);
suf[z] = x;
pre[x] = z;
}
if (rc[x])
{
int z = findRight(rc[x], x);
Splay(z, x);
Clear(lc[z]);
pre[z] = x;
suf[x] = z;
}
if (!Slope1(pre[x], x, suf[x]))
{
rt = lc[x];
rc[rt] = rc[x];
fa[rc[x]] = rt;
fa[rt] = 0;
lc[x] = rc[x] = fa[x] = 0;
suf[rt] = rc[rt];
pre[rc[rt]] = rt;
}
}
inline int Query(ll z)
{
int x = rt, res = 0;
while (x)
{
if (Slope2(pre[x], x, z))
res = x, x = rc[x];
else
x = lc[x];
}
return Splay(res, 0), res;
}
}
- 还可以换一种表示形式:
f i = max / min { B ( j ) A ( i ) + D ( j ) + f j } + C ( i ) f_i = \max /\min \{B(j)A(i) + D(j) + f_j\} + C(i) fi=max/min{B(j)A(i)+D(j)+fj}+C(i)
- max / min \max / \min max/min 内部的式子可以看作是若干条以 B ( j ) B(j) B(j) 为斜率、 D ( j ) + f j D(j) + f_j D(j)+fj 为截距的直线, 即可用李超线段树维护,相较于平衡树的写法要简单很多,代码见数据结构部分。
- 若采用动态开点的写法,还可支持线段树合并,因而还可支持子树形式的转移。
决策单调性
四边形不等式
- w ( x , y ) w(x,y) w(x,y) 为定义在整数集合上的二元函数,对于定义域上的任意整数 a , b , c , d a,b,c,d a,b,c,d,其中 a ≤ b ≤ c ≤ d a\le b \le c\le d a≤b≤c≤d,都有 w ( a , d ) + w ( b , c ) ≥ w ( a , c ) + w ( b , d ) w(a,d) + w(b,c) \ge w(a,c) + w(b,d) w(a,d)+w(b,c)≥w(a,c)+w(b,d),称为函数 w w w 满足四边形不等式。
一维决策单调性
- 在状态转移方程 f i = min 0 ≤ j < i { f j + w ( j , i ) } f_i = \min\limits_{0 \le j < i}\{f_j + w(j,i)\} fi=0≤j<imin{fj+w(j,i)} 中,若函数 w w w 满足四边形不等式,则 f f f 具有决策单调性。
- 用队列维护若干个三元组 ( l , r , j ) (l,r,j) (l,r,j),表示 [ l , r ] [l,r] [l,r] 内的最优决策均为 j j j。
- 在队尾插入时:
- 若比前一个三元组整个区间内的决策都优,则直接合并,继续检查前一个三元组。
- 若比前一个三元组整个区间内的决策都不优,则直接插入队尾。
- 否则在区间上二分找到分界点,修改两个区间的分界后插入队尾。
二维决策单调性
- 在状态转移方程
f
i
,
j
=
min
i
≤
k
<
j
{
f
i
,
k
+
f
k
+
1
,
j
+
w
(
i
,
j
)
}
f_{i,j} = \min\limits_{i \le k < j}\{f_{i,k}+f_{k+1,j}+w(i,j)\}
fi,j=i≤k<jmin{fi,k+fk+1,j+w(i,j)} 中,若下面三个条件成立:
- 可规定 f i , i = w ( i , i ) = 0 f_{i,i} = w(i,i) = 0 fi,i=w(i,i)=0。
- w w w 满足四边形不等式。
- 对于任意的 a ≤ b ≤ c ≤ d a \le b \le c \le d a≤b≤c≤d,有 w ( a , d ) ≥ w ( b , c ) w(a,d) \ge w(b,c) w(a,d)≥w(b,c)。
- 则 f f f 也满足四边形不等式,设 p i , j p_{i,j} pi,j 表示令 f i , j f_{i,j} fi,j 取到最小值的 k k k 值,则恒有 p i , j − 1 ≤ p i , j ≤ p i + 1 , j p_{i,j-1}\le p_{i,j}\le p_{i + 1,j} pi,j−1≤pi,j≤pi+1,j。
- 因此我们在转移时只需枚举
[
p
i
,
j
−
1
,
p
i
+
1
,
j
]
[p_{i,j-1},p_{i+1,j}]
[pi,j−1,pi+1,j] 内的
k
k
k,对于长度为
L
+
1
L+1
L+1 的区间,总枚举量为:
( p 2 , L + 1 − p 1 , L ) + ( p 3 , L + 2 − p 2 , L + 1 ) + ⋯ + ( p n − L + 1 , n − p n − L , n − 1 ) = p n − L + 1 , n − p 1 , L (p_{2, L+1} - p_{1,L})+(p_{3,L+2}-p_{2,L+1})+\dots+(p_{n - L + 1, n} - p_{n - L, n - 1}) = p_{n - L + 1, n} - p_{1,L} (p2,L+1−p1,L)+(p3,L+2−p2,L+1)+⋯+(pn−L+1,n−pn−L,n−1)=pn−L+1,n−p1,L - 因此总的时间复杂度为 O ( ∑ i = 1 n − 1 ( p n − i + 1 , n − p 1 , i ) ) = O ( n 2 ) \mathcal O(\sum \limits_{i = 1}^{n - 1}(p_{n-i+1,n} - p_{1,i})) = \mathcal O(n^2) O(i=1∑n−1(pn−i+1,n−p1,i))=O(n2)。
- 形如 f i , j = max i ≤ k ≤ j / min i ≤ k ≤ j { f i , k + f k , j + C } , C ∈ R f_{i,j} = \max\limits_{i \le k \le j}/\min\limits_{i \le k \le j}\{f_{i,k} + f_{k,j}+C\}, C \in \mathbb R fi,j=i≤k≤jmax/i≤k≤jmin{fi,k+fk,j+C},C∈R 也可以套用上述流程优化。
GarsiaWachs 算法
- 上述二维决策单调性能解决的石子合并问题有一种专门的非动态规划算法。
- 设第
i
i
i 堆石子的数目为
a
i
a_i
ai,令
a
0
=
a
n
+
1
=
+
∞
a_0 = a_{n + 1} = +\infin
a0=an+1=+∞,具体步骤如下:
- 找到满足 a k − 1 < a k + 1 a_{k - 1} < a_{k + 1} ak−1<ak+1 最大的 k k k。
- 找到满足 a j > a k − 1 + a k a_j > a_{k - 1} + a_k aj>ak−1+ak 且 j < k j < k j<k 的最大的 j j j。
- 删除 a k − 1 , a k a_{k - 1}, a_k ak−1,ak,在 a j a_j aj 后插入 a k + a k − 1 a_k + a_{k - 1} ak+ak−1。
- 重复上述过程直至石子被合并为一堆。
- 空间复杂度 O ( n ) \mathcal O(n) O(n),时间复杂度 O ( n 2 ) \mathcal O(n^2) O(n2),可用平衡树优化至 O ( n log n ) \mathcal O(n\log n) O(nlogn)。
证明 待补充。
read(n);
v.push_back(Maxn);
for (int i = 1, x; i <= n; ++i)
{
read(x);
v.push_back(x);
}
v.push_back(Maxn);
while (n > 1)
{
int j, k;
for (k = 1; k <= n; ++k)
if (v[k - 1] < v[k + 1])
break ;
for (j = k - 1; j >= 0; --j)
if (v[j] > v[k - 1] + v[k])
break ;
int sum = v[k - 1] + v[k];
v.erase(v.begin() + k - 1);
v.erase(v.begin() + k - 1);
v.insert(v.begin() + j + 1, sum);
ans += sum;
--n;
}
printf("%d\n", ans);
整除分块优化 DP
典例 HDU7217
题目大意
- 对长度不超过 n n n 且序列中最大值不超过 m m m 的序列 a a a 取模计数,设序列长度为 l l l,则 ∀ 1 ≤ i < l , a i ∣ a i + 1 \forall 1 \le i < l, a_i | a_{i + 1} ∀1≤i<l,ai∣ai+1。
- n , m ≤ 1 0 9 n,m \le 10^9 n,m≤109。
解法
-
若序列 a a a 中各元素互不相同,则序列 a a a 的长度不会超过 1 + ⌊ log 2 m ⌋ 1 + \lfloor \log_2 m\rfloor 1+⌊log2m⌋,因而只需统计各元素互不相同的序列 a a a 的方案,再通过乘组合数插回去即可,设当前长度为 t t t,则插回长度不超过 n n n 的序列的方案数为 ∑ i = t n ( i − 1 t − 1 ) = ( n t ) \sum \limits_{i = t}^{n}\dbinom{i - 1}{t - 1} = \dbinom{n}{t} i=t∑n(t−1i−1)=(tn)。
-
设 f t , x f_{t,x} ft,x 表示从大到小填数,序列已经填了 t t t 个数字,目前填的最小的数字最大不超过 x x x 的方案数,有转移: ∀ 2 ≤ d ≤ x , f t + 1 , ⌊ x d ⌋ ← f t , x \forall 2 \le d \le x, f_{t + 1, \lfloor \frac{x}{d}\rfloor} \leftarrow f_{t,x} ∀2≤d≤x,ft+1,⌊dx⌋←ft,x
-
初始时 f 1 , m = 1 f_{1,m} = 1 f1,m=1,最后答案为 ∑ ( f t , x × x × ( n t ) ) \sum \left(f_{t,x} \times x \times \dbinom{n}{t}\right) ∑(ft,x×x×(tn))。
-
可通过整除分块优化,注意到所有转移到的 x x x 都可以表示为 ⌊ m d ⌋ \lfloor \dfrac{m}{d}\rfloor ⌊dm⌋ 的形式,总共只有 2 m 2\sqrt m 2m 个状态,类似 Min_25 筛将 ⌊ m d ⌋ \lfloor \dfrac{m}{d}\rfloor ⌊dm⌋ 按照是否大于 m \sqrt m m 分类即可进行离散化编号,在转移过程中统计答案即可,时间复杂度为 O ( m 3 4 ) \mathcal O(m^{\frac{3}{4}}) O(m43),分析与杜教筛的一致。
凸优化 DP
- 一般这类题目会要求选出恰好 K K K 件物品的最优代价 f ( K ) f(K) f(K),但 K K K 较大不能记录在 DP 状态中,且可证明答案函数关于 K K K 是凸函数。
- 由费用流模型中关于流量的费用函数是凸的,如能设计出一个理论上的费用流模型,即可证明答案函数是凸函数。
- 以最大化代价为例,考虑二分恰好能在答案函数上截到点 ( K , f ( K ) ) (K, f(K)) (K,f(K)) 的直线斜率 C C C,就将问题转化为选择每件物品时有额外的代价 − C -C −C,要最大化总代价,设最优化代价时对应的物品件数为 g ( C ) g(C) g(C),而二分的结果为满足 g ( C ) ≥ K g(C) \ge K g(C)≥K 的最大的 C C C(记为 C 0 C_0 C0)。最后答案即为端点为 ( g ( C 0 + 1 ) , f ( g ( C 0 + 1 ) ) ) (g(C_0 + 1), f(g(C_0 + 1))) (g(C0+1),f(g(C0+1))) 和 ( g ( C 0 ) , f ( g ( C 0 ) ) ) (g(C_0),f(g(C_0))) (g(C0),f(g(C0))) 的线段在 x = K x = K x=K 处的取值。
计算几何
基础操作
- 判断点 A A A 在线段 P Q PQ PQ 上 即判断 ∣ A P ∣ + ∣ A Q ∣ = ∣ P Q ∣ |AP| + |AQ| = |PQ| ∣AP∣+∣AQ∣=∣PQ∣。
- 向量的旋转 将向量 a ⃗ = ( x , y ) \vec{a} = (x,y) a=(x,y) 逆时针旋转 θ \theta θ 得到向量 b ⃗ = ( x cos θ − y sin θ , x sin θ + y cos θ ) \vec{b} = (x\cos \theta - y\sin \theta, x\sin \theta + y\cos\theta) b=(xcosθ−ysinθ,xsinθ+ycosθ),由三角函数的和角公式可证。
- 曼哈顿距离与切比雪夫距离
- 二维曼哈顿距离为 ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ |x_1 - x_2| + |y_1 - y_2| ∣x1−x2∣+∣y1−y2∣,切比雪夫距离为 max { ∣ x 1 − x 2 ∣ , ∣ y 1 − y 2 ∣ } \max\{|x_1 - x_2|,|y_1 - y_2|\} max{∣x1−x2∣,∣y1−y2∣}
- ( x , y ) (x,y) (x,y) 坐标下的曼哈顿距离,等同于 ( x + y , x − y ) (x + y, x - y) (x+y,x−y) 坐标下的切比雪夫距离。
- ( x , y ) (x, y) (x,y) 坐标下的切比雪夫距离,等同于 ( x + y 2 , x − y 2 ) (\frac{x + y}{2},\frac{x - y}{2}) (2x+y,2x−y) 坐标下的曼哈顿距离。
欧拉公式
- 在任何一个规则球面地图上,定义
R
R
R 为区域个数,
V
V
V 为顶点个数,
E
E
E 为边界个数,则恒有:
R + V − E = 2 R+V-E=2 R+V−E=2 - 推论 凸正多面体(柏拉图立体)有且仅有 5 种。
证明 取凸正多面体一个顶点,设其向外有 n ( n ≥ 3 ) n(n\ge 3) n(n≥3) 条棱,再取凸正多面体一个面,设其为正 m ( m ≥ 3 ) m(m\ge 3) m(m≥3) 边形。则
n V = 2 E ⇔ V = 2 E n m R = 2 E ⇔ R = 2 E m nV=2E \Leftrightarrow V = \dfrac{2E}{n}\\ mR=2E \Leftrightarrow R = \dfrac{2E}{m}\\ nV=2E⇔V=n2EmR=2E⇔R=m2E
带入欧拉公式,得
1 m + 1 n = 1 E + 1 2 \dfrac{1}{m}+\dfrac{1}{n}=\dfrac{1}{E}+\dfrac{1}{2} m1+n1=E1+21
注意到 m , n m,n m,n 不能同时大于 3,且其中一个等于 3 时另一个不能超过 5。
符合条件的 m , n m,n m,n 的解只有五组,如下图所示。
- 设棱长为 a a a,五种凸正多面体的各项数据如下(棱切球即过各边中点的球)。
正四面体 | 立方体 | 正八面体 | 正十二面体 | 正二十面体 | |
---|---|---|---|---|---|
R R R | 4 | 6 | 8 | 12 | 20 |
V V V | 4 | 8 | 6 | 20 | 12 |
E E E | 6 | 12 | 12 | 30 | 30 |
内切球半径 | 6 12 a \frac{\sqrt 6}{12}a 126a | 1 2 a \frac{1}{2}a 21a | 6 6 a \frac{\sqrt 6}{6}a 66a | 1 2 5 2 + 11 10 5 a \frac{1}{2}\sqrt{\frac{5}{2}+\frac{11}{10}\sqrt 5}a 2125+10115a | 3 3 + 15 12 a \frac{3\sqrt 3 + \sqrt {15}}{12}a 1233+15a |
外接球半径 | 6 4 a \frac{\sqrt 6}{4}a 46a | 3 2 a \frac{\sqrt 3}{2}a 23a | 2 2 a \frac{\sqrt 2}{2}a 22a | 3 4 ( 1 + 5 ) a \frac{\sqrt 3}{4}(1+\sqrt 5)a 43(1+5)a | 10 + 2 5 4 a \frac{\sqrt{10 + 2\sqrt 5}}{4}a 410+25a |
棱切球半径 | 2 4 a \frac{\sqrt 2}{4}a 42a | 2 2 a \frac{\sqrt 2}{2}a 22a | 1 2 a \frac{1}{2}a 21a | 3 + 5 4 a \frac{3 + \sqrt 5}{4}a 43+5a | 1 + 5 4 a \frac{1+\sqrt 5}{4}a 41+5a |
表面积 | 3 a 2 \sqrt {3}a^2 3a2 | 6 a 2 6a^2 6a2 | 2 3 a 2 2\sqrt{3}a^2 23a2 | 3 25 + 10 5 a 2 3\sqrt{25+10\sqrt{5}}a^2 325+105a2 | 5 3 a 2 5\sqrt{3}a^2 53a2 |
体积 | 2 12 a 3 \frac{\sqrt{2}}{12}a^3 122a3 | a 3 a^3 a3 | 2 3 a 3 \frac{\sqrt{2}}{3}a^3 32a3 | 15 + 7 5 4 a 3 \frac{15+7\sqrt{5}}{4}a^3 415+75a3 | 15 + 5 5 12 a 3 \frac{15+5\sqrt{5}}{12}a^3 1215+55a3 |
Pick 定理
- 定义 对于一个所有顶点均为整点的简单多边形,定义 S S S 为这个多边形的面积, i i i 为严格在这个多边形内部的格点数, b b b 为在这个多边形边上的格点数,则三者满足关系式 S = i + b 2 − 1 S = i + \frac{b}{2} - 1 S=i+2b−1。
证明 考虑证明 引理1 和 引理3,则 Pick 定理显然成立。
- 引理1 若两个只有一条公共边的多边形满足 Pick 定理,则将两个多边形去掉公共边,合并成的一个多边形也满足 Pick 定理。
证明 设合并的两个多边形为 P , Q P,Q P,Q,它们的公共边上的格点数(不包括端点)为 c c c。
S = S P + S Q = i P + i Q + b P + b Q 2 − 2 = i − c + b + 2 c + 2 2 − 2 = i + b 2 − 1 \begin{aligned}S &= S_P + S_Q \\&=i_P + i_Q + \frac{b_P + b_Q}{2} - 2\\&= i - c + \frac{b + 2c + 2}{2} - 2\\& =i + \frac{b}{2} - 1 \\\end{aligned} S=SP+SQ=iP+iQ+2bP+bQ−2=i−c+2b+2c+2−2=i+2b−1
- 引理2 有两个只有一条公共边的多边形,若将这两个多边形去掉公共边,合并成的一个多边形满足 Pick 定理,且这两个多边形中有一个满足 Pick 定理,则另一个多边形也满足 Pick 定理。
证明 用类似 引理1 的方法即可。
- 引理3 任意一个三角形都满足 Pick 定理。
证明
已知面积为 1 的正方形满足 Pick 定理,由 引理1 得任意大小的矩形都满足 Pick 定理。
将一个矩形拆分为两个全等的直角三角形,证明任意一个直角三角形都满足 Pick 定理。
证明 设两个直角三角形公共边上的格点数(不包括端点)为 c c c,原矩形为 R R R,拆分出的直角三角形为 T T T。
S T = S R 2 = i R 2 + b R 4 − 1 2 = 2 i T + c 2 + 2 b T − 2 c − 2 4 − 1 2 = i T + b T 2 − 1 \begin{aligned}S_T &= \frac{S_R}{2} \\ &= \frac{i_R}{2} + \frac{b_R}{4} - \frac{1}{2} \\&= \frac{2i_T + c}{2} + \frac{2b_T - 2c - 2}{4} - \frac{1}{2} \\&= i_T + \frac{b_T}{2} - 1\\\end{aligned} ST=2SR=2iR+4bR−21=22iT+c+42bT−2c−2−21=iT+2bT−1任意一个三角形显然可以由一个矩形拆去不多于3个的直角三角形得到,由 引理2 可知得证。
凸包
- 常见的求法为 Graham \text{Graham} Graham 扫描法,先找出最左下角的点,将其余点按照相对于该点的极角排序(实现时不需要真的求出极角,只需要通过叉积判断即可),然后维护一个栈,将点按照极角序加入该栈,同样通过叉积判断,若不满足凸包的形态则弹栈,最后即能求出凸包。
闵可夫斯基和
- 定义点的加法为对应坐标相加,则点集
A
A
A 和点集
B
B
B 的闵可夫斯基和
{ x + y ∣ x ∈ A , y ∈ B } \{x+y|x\in A,y\in B\} {x+y∣x∈A,y∈B} - 定义凸集
C
C
C(即轮廓是凸包的区域)
∀ x , y ∈ C , ∀ λ ∈ [ 0 , 1 ] , λ x + ( 1 − λ ) y ∈ C \forall x,y\in C,\forall \lambda\in[0,1],\lambda x + (1 - \lambda)y \in C ∀x,y∈C,∀λ∈[0,1],λx+(1−λ)y∈C - 容易根据定义证明两个凸集的闵可夫斯基和仍然是凸集,因此两个点集的闵可夫斯基和的凸包可通过两个凸包的闵可夫斯基和得到。
- 具体求解相当于将一个凸包绕着另一个凸包的轮廓转一圈,可通过双指针实现。
typedef long long ll;
const int N = 4e5 + 5;
int top;
struct point
{
ll x, y;
point() {}
point(ll X, ll Y):
x(X), y(Y) {}
inline void scan()
{
int _x, _y;
cin >> _x >> _y;
x = _x, y = _y;
}
inline bool operator < (const point &a) const
{
return x < a.x || x == a.x && y < a.y;
}
inline ll dist() const
{
return x * x + y * y;
}
inline point operator + (const point &a) const
{
return point(x + a.x, y + a.y);
}
inline point operator - (const point &a) const
{
return point(x - a.x, y - a.y);
}
inline ll operator * (const point &a) const
{
return x * a.y - y * a.x;
}
}stk[N];
inline bool cmp(const point &x, const point &y)
{
ll del = x * y;
if (del == 0)
return x.dist() < y.dist();
else
return del > 0;
}
struct hull
{
point p[N];
int n;
inline void normal() // Graham 扫描法求凸包
{
int id = 1;
for (int i = 2; i <= n; ++i)
if (p[i] < p[id])
id = i;
if (id != 1)
std::swap(p[id], p[1]);
for (int i = n; i >= 2; --i)
p[i] = p[i] - p[1];
std::sort(p + 2, p + n + 1, cmp);
stk[top = 1] = point(0, 0);
for (int i = 2; i <= n; ++i)
{
while (top > 1 && (p[i] - stk[top - 1]) * (stk[top] - stk[top - 1]) >= 0) --top;
stk[++top] = p[i];
}
n = top;
for (int i = 2; i <= top; ++i)
p[i] = stk[i] + p[1];
}
inline void scan(int _n)
{
n = _n;
for (int i = 1; i <= n; ++i)
p[i].scan();
normal();
}
inline hull operator + (hull a)
{
hull f;
f.p[f.n = 1] = p[1] + a.p[1];
int i = 1, j = 1;
while (i <= n || j <= a.n)
{
point tx = p[(i - 1) % n + 1] + a.p[j % a.n + 1],
ty = p[i % n + 1] + a.p[(j - 1) % a.n + 1];
if ((tx - f.p[f.n]) * (ty - f.p[f.n]) >= 0)
++j, f.p[++f.n] = tx;
else
++i, f.p[++f.n] = ty;
}
for (int i = 2; i <= f.n; ++i)
f.p[i] = f.p[i] - f.p[1];
stk[top = 1] = point(0, 0);
for (int i = 2; i <= f.n; ++i)
{
while (top > 1 && (f.p[i] - stk[top - 1]) * (stk[top] - stk[top - 1]) >= 0) --top;
stk[++top] = f.p[i];
}
f.n = top;
for (int i = 2; i <= top; ++i)
f.p[i] = stk[i] + f.p[1];
--f.n; //最后一个点一定和第一个点相同
return f;
}
};
- 若是求上/下凸壳,写法会有些不同。
struct hull
{ //求一个上凸壳,使得其余点均在该凸壳下方
point p[N];
int n;
inline void normal()
{
top = 0;
for (int i = 1; i <= n; ++i)
{
while (top > 1 && (p[i] - stk[top - 1]) * (stk[top] - stk[top - 1]) <= 0)
--top;
stk[++top] = p[i];
}
n = top;
for (int i = 1; i <= top; ++i)
p[i] = stk[i];
}
inline hull operator + (hull a)
{
hull f;
f.p[f.n = 1] = p[1] + a.p[1];
int i = 1, j = 1;
while (i < n && j < a.n)
{
point tx = p[i] + a.p[j + 1],
ty = p[i + 1] + a.p[j];
if ((tx - f.p[f.n]) * (ty - f.p[f.n]) <= 0)
++j, f.p[++f.n] = tx;
else
++i, f.p[++f.n] = ty;
}
while (i < n)
f.p[++f.n] = p[++i] + a.p[j];
while (j < a.n)
f.p[++f.n] = p[i] + a.p[++j];
f.normal();
return f;
}
};