UVA 1490 Let the light guide us 题解

题意

有一个 \(n\times m\) 的网格,每个格子有两个值 \(a\)\(b\)(原题中是 \(T\)\(f\))。你需要在每一行选择一个格子,使得对于任意选择的两个格子 \((i, j), (i+1, k)\),满足 \(\lvert j - k \rvert \le b(i,j) + b(i + 1,k)\)。求所选格子的 \(a\) 之和的最小值。

\(n \le 100, m \le 5000, 0 \le b(i, j) \le 10^5\)

思路

朴素 DP

首先,显然 \(b > 5000\) 是没有意义的(因为 \(\lvert j - k\rvert\) 是不可能大于 \(5000\) 的)。

然后可以设计出一个状态:\(f(i, j)\) 表示考虑了前 \(i\) 行,第 \(i\) 行选了第 \(j\) 列的格子,最小的代价。转移方程是这样的:

\[ f(i, j) = a(i, j) + \min_{\lvert j - k \rvert \le b(i,j) + b(i-1,k)} f(i - 1, k) \]

时间复杂度 \(O(nm^2)\)

优化

这个状态基本上已经无法优化了(反正我不会),所以我们要在转移上下手。

我们看这个式子:

\[ \lvert j - k\rvert \le b(i, j) + b(i - 1, k) \]

显然右边大于 \(0\),所以可以把它化成这样:(两个式子同时成立)

\[ \begin{cases} j - k \le b(i, j) + b(i - 1, k)\\ k - j \le b(i, j) + b(i - 1, k) \end{cases} \]

化简得:

\[ \begin{cases} j - b(i, j) \le k + b(i - 1, k)\\ j + b(i, j) \ge k - b(i - 1, k) \end{cases} \]

接下来就比较神奇了。(鬼知道这是怎么想到的)

我们记 \(S_L = j - b(i, j)\)\(S_R = j + b(i, j)\),并记区间 \(S = [S_L, S_R]\)
类似的,记 \(T_L = k - b(i - 1, k)\)\(T_R = k + b(i - 1, k)\),并记区间 \(T = [T_L, T_R]\)
(显然 \(S_L \le S_R\)\(T_L \le T_R\)

那么上面的式子就可以写成:

\[ \begin{cases} S_L \le T_R\\ T_L \le S_R \end{cases} \]

那么这个式子和「\(S\)\(T\) 相交」是等价的。

证明

区间 \(S\)\(T\) 不相交 等价于 \(S_R < T_L\)\(T_R < S_L\),所以 \(S\)\(T\) 相交 等价于:

\[ \begin{cases} S_R \ge T_L\\ T_R \ge S_L \end{cases} \]

这与原式相同。

然后就简单了。现在的问题是对于每个格子有一个区间 \([S_L, S_R]\),如果相邻两行的格子的区间相交,那么这两个格子就可以同时被选(即可以用上一行格子的 \(dp\) 值更新这一行的 \(dp\) 值)。

做法显然是对于每一行开一个线段树,将上一行的 \([T_L, T_R]\) 区间取 \(\min\),这一行的 \([S_L, S_R]\) 区间求 \(\min\),就是区间改区间查的问题了。

\(n\) 个线段树空间会炸,但其实可以只用开一个线段树。

时间复杂度 \(O(nm\log m)\)

代码

代码

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
65
66
67
68
69
// UVA1490
#include <cstdio>
#include <algorithm>
#include <cstring>

const int N = 100 + 5;
const int M = 5000 + 5;
const int ARR_V = 7000 + 5;
const int V_MAX = 6000;
const int INF = 0x3f3f3f3f;

int a[N][M], b[N][M];
int n, m;

int f[N][M];

struct SegmentTree {
int t[ARR_V << 3], lazy[ARR_V << 3];
void reset() {
memset(t, 0, sizeof(t));
memset(lazy, INF, sizeof(lazy));
}
void build() {
memset(t, INF, sizeof(t));
memset(lazy, INF, sizeof(lazy));
}
void set_lazy(int x, int v) { t[x] = std::min(t[x], v), lazy[x] = std::min(lazy[x], v); }
void lazy_down(int x) {
set_lazy(x << 1, lazy[x]), set_lazy(x << 1 | 1, lazy[x]);
lazy[x] = INF;
}
int query(int ql, int qr, int x = 1, int l = 1, int r = V_MAX << 1) {
if(ql > qr) return INF;
if(ql <= l && r <= qr) return t[x];
int mid = (l + r) >> 1, ret = INF;
lazy_down(x);
if(ql <= mid) ret = std::min(ret, query(ql, qr, x << 1, l, mid));
if(qr > mid) ret = std::min(ret, query(ql, qr, x << 1 | 1, mid + 1, r));
return ret;
}
void modify(int ql, int qr, int qv, int x = 1, int l = 1, int r = V_MAX << 1) {
if(ql > qr) return;
if(ql <= l && r <= qr) { set_lazy(x, qv); return; }
int mid = (l + r) >> 1;
lazy_down(x);
if(ql <= mid) modify(ql, qr, qv, x << 1, l, mid);
if(qr > mid) modify(ql, qr, qv, x << 1 | 1, mid + 1, r);
t[x] = std::min(t[x << 1], t[x << 1 | 1]);
}
} seg;

int main() {
while(scanf("%d%d", &n, &m) == 2 && (n | m)) {
for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) scanf("%d", &a[i][j]);
for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) scanf("%d", &b[i][j]), b[i][j] = std::min(b[i][j], 5000);
seg.reset();
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++)
f[i][j] = seg.query(j - b[i][j] + V_MAX, j + b[i][j] + V_MAX) + a[i][j];
seg.build();
for(int j = 1; j <= m; j++)
seg.modify(j - b[i][j] + V_MAX, j + b[i][j] + V_MAX, f[i][j]);
}
int ans = INF;
for(int j = 1; j <= m; j++) ans = std::min(ans, f[n][j]);
printf("%d\n", ans);
}
return 0;
}