单纯的暴力搜索就不讲了。
技巧
1.1 折半搜索
这个东西有另外一个名称叫 M e e t i n t h e m i d d l e \mathrm{Meet~in~the~middle} Meet in the middle 。
这个东西可以解决整个规模比搜索可以解决的规模大一倍的题目,这样我们可以将枚举分成前一部分和后一部分,然后将前一部分和后一部分的答案结合起来就可以了。当然了,为了将答案合起来,我们记住的东西一定要有类似结合率的东西(废话)
E x a m p l e 1 \mathrm{Example~1} Example 1 大容量背包问题
有 N N N 个物品,背包有 M M M 的容量,其中第 i i i 个物品的体积和价值分别为 w i , v i w_i, v_i wi,vi,求最大价值。
N ≤ 40 , w i ≤ M ≤ 1 0 9 N \leq 40, w_i \leq M \leq 10^9 N≤40,wi≤M≤109
可以发现,直接搜索是 2 40 2 ^ {40} 240 的,一定会超时,但是众所周知,背包是可以合并的,那么我们可以将物品分成前后两部分,每次 d f s \mathrm{dfs} dfs 时候记住当前已经选择了多少的体积,以及对应的价值。然后将两部分的答案存起来,然后用 T w o p o i n t e r s \mathrm{Two~pointers} Two pointers 合并。
1.2 剪枝
这个东西最为玄妙,用得好的话可以拿到不错的暴力分。一般认为剪枝有两种,一种是可行性剪枝,另一种是最优化剪枝。
可行性剪枝比较简单易懂,比如 E x a m p l e 1 \mathrm{Example~1} Example 1 中,当枚举到的总体积一定大于背包容量的时候,退掉这一部分的搜索。
最优化剪枝用下面的例子进行讲解。
E x a m p l e 2 \mathrm{Example~2} Example 2 [NOI1999]生日蛋糕
给定一个总体积 π N \pi N πN 和层数 M M M,构建一个蛋糕,每一层都是圆柱形,你需要规定由下往上看,每一层的半径 R i R_i Ri 和高度 H i H_i Hi,要求对于所有 1 ≤ i < m 1 \leq i < m 1≤i<m,都是 R i > R i + 1 , H i > H i + 1 R_i > R_{i+1}, H_i > H_{i+1} Ri>Ri+1,Hi>Hi+1。
上述的所有数都是正整数。
现在要求除去底面积之后最小的表面积。
为了书写方便,将所有的 π \pi π 都略掉了,也就是将圆形底面换成正方形底面。
首先,现在要求的表面积等于侧面积 + + +上层面积,而上层面积等于底面积,所以直接在枚举最底层的时候加上底面积就好了。
最简单的暴力就是从下往上枚举每一层的高度和半径,但是这样只能拿 10 ~ 20 p t s 10~20\mathrm{pts} 10~20pts。
考虑剪枝:
首先,第
i
i
i 层可以枚举的半径就是: (其中
s
V
sV
sV 表示之前已经枚举的所有层的总体积除以
π
\pi
π。)
[
M
−
i
+
1
,
min
{
⌊
N
−
s
V
⌋
,
R
i
−
1
−
1
}
]
\left[ M-i+1, \min\left\{ \left\lfloor \sqrt{N-sV} \right\rfloor, R_{i-1}-1 \right \} \right]
[M−i+1,min{⌊N−sV⌋,Ri−1−1}]
第
i
i
i 层可以枚举的高度也是差不多的:
[
M
−
i
−
1
,
min
{
⌊
N
−
s
V
R
i
2
⌋
,
H
i
−
1
−
1
}
]
\left[ M-i-1, \min \left\{ \left\lfloor \frac{N-sV}{R_i^2} \right\rfloor, H_{i-1}-1 \right\} \right]
[M−i−1,min{⌊Ri2N−sV⌋,Hi−1−1}]
还有一个,当
s
V
>
N
sV > N
sV>N ,也是可以直接剪枝的(废话)。
还有另一个可行性剪枝:当前面积 + + +之后可以得到的最大面积 < N <N <N,即 s S + R i 2 H i × ( M − i ) < N sS+R_i^2 H_i\times(M-i) < N sS+Ri2Hi×(M−i)<N,就可以退出了(之后所有层的体积都是小于 R i 2 H i R_i^2 H_i Ri2Hi 的)
接下来是最优化剪枝,这个东西有个套路:
当
前
的
答
案
+
之
后
的
最
优
答
案
(
忽
略
某
些
限
制
条
件
)
≤
当
前
得
到
的
最
优
答
案
当前的答案+之后的最优答案(忽略某些限制条件) \leq 当前得到的最优答案
当前的答案+之后的最优答案(忽略某些限制条件)≤当前得到的最优答案
如果不满足这个条件,就直接剪枝。
在这道题中,当前答案 + + +之后的最小侧面积 > > >已经得到的最小面积,就直接剪枝。
形式化讲 s S + M − i + 1 > a n s sS + M - i + 1 > ans sS+M−i+1>ans 就可以剪枝了(令之后的所有层都是半径和高度都是 1 1 1 )
同时可以利用之前枚举出来的 R i , H i R_i,H_i Ri,Hi,众所周知,当前总体积和总侧面积可以使用下面的公式求出来(废话):
s V = ∑ j = 1 i R j 2 H j s S = 2 × ∑ j = 1 i R j H j sV=\sum\limits_{j=1}^i R_j^2 H_j\\ sS=2\times \sum\limits_{j=1}^i R_j H_j sV=j=1∑iRj2HjsS=2×j=1∑iRjHj
那么对于 [ i + 1 , M ] [i+1,M] [i+1,M]的总体积和表面积,就有:
N − s V = ∑ j = i + 1 M R j 2 H j s S ′ = 2 × ∑ j = i + 1 M R j H j N-sV=\sum\limits_{j=i+1}^M R_j^2 H_j\\ sS'=2\times \sum\limits_{j=i+1}^M R_j H_j N−sV=j=i+1∑MRj2HjsS′=2×j=i+1∑MRjHj
那么可以得到:
s
S
′
=
2
R
i
∑
j
=
i
+
1
M
R
j
R
i
H
j
≥
2
R
i
∑
j
=
i
+
1
M
R
j
2
H
j
=
2
(
N
−
s
V
)
R
i
sS'=\frac{2}{R_i}\sum\limits_{j=i+1}^M R_j R_i H_j \ge \frac{2}{R_i}\sum\limits_{j=i+1}^M R_j^2 H_j =\frac{2(N-sV)}{R_i}
sS′=Ri2j=i+1∑MRjRiHj≥Ri2j=i+1∑MRj2Hj=Ri2(N−sV)
也就是说,当 s S + 2 ( N − s V ) R i > a n s sS + \frac{2(N - sV)}{R_i} > ans sS+Ri2(N−sV)>ans ,那么就直接剪枝。
应用
f l o o d f i l l \mathrm{flood~fill} flood fill 灌水法
这个东西的名字暗示了这篇 B l o g \mathrm{Blog} Blog的本质。
灌水法可以用于无向图联通块的信息统计,最直接的应用就是网格图的染色。
E x a m p l e 3 \mathrm{Example~3} Example 3 S i m p l e T a s k \mathrm{Simple~Task} Simple Task
随便口胡一道题。
在一个 N × M N\times M N×M 的网格图中,有一些块是有颜色的,而有一些位置是障碍,现在需要求出至少包含 K K K 种颜色的最大联通块。
N , M ≤ 1 0 3 , 1 ≤ 颜 色 编 号 ≤ K ≤ 1 0 4 N,M\leq 10^3,1 \leq 颜色编号\leq K\leq 10^4 N,M≤103,1≤颜色编号≤K≤104
可以发现,就是每次找到一个之前没有深搜到的不是障碍的点开始,然后向四周扩散,然后记住拓展了多少个联通块,如果存在一个这个联通块中没有的颜色,就颜色数加一。
但是每次向新的联通块中灌水之前似乎需要清空记住颜色的数组。但是如果使用 memset
就会
T
L
E
\mathrm{TLE}
TLE,如果使用 STL
就会带个
log
2
\log_2
log2 (unordered_map
在
N
O
I
P
\mathrm{NOIP}
NOIP 中不建议使用),这时候可以使用比较没用的时间戳法,记住颜色数组中每一个位置最新一次修改的时间,比如这道题中可以使用当前
d
f
s
\mathrm{dfs}
dfs 第几个联通块作为时间点,当某个位置记录的时间点小于当前之间点,就先将这个位置清零,然后再改成新的值。
比较傻
const int maxk = 1e4 + 5;
const int vec[5][2] = {{0,0},{0,1},{0,-1},{1,0},{-1,0}};
int colfl[maxk], tchk[maxk]/*时间戳*/, ti/*总的时间点*/;
int cnt1, cnt2, col[maxn][maxn]/*颜色,如果col[i][j] == -1 就表示(i,j)是障碍*/
int ask_colfl(int pos) {
if (tchk[pos] < ti) tchk[pos] = ti, colfl[pos] = 0;
return colfl[pos];
}
void chg_colfl(int pos, int val) {
colfl[pos] = val, tchk[pos] = ti;
}
void dfs(int x, int y) {
cnt1++;
if (!ask_colfl(col[x][y]))
chg_colfl(col[x][y], val), cnt2++;
for (int i = 1; i <= 4; i++) {
int nex_x = x + vec[i][0], nex_y = y + vec[i][1];
if (!nex_x || !nex_y || nex_x > n || nex_y > m) continue;
if (col[nex_x][nex_y] == -1) continue;
dfs(nex_x, nex_y);
}
}
//main()中的代码
int ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (!vis[i][j] && col[i][j] == -1) {
ti++, cnt1 = cnt2 = 0, dfs(i, j);
if (cnt2 >= k) ans = max(ans, cnt1);
}