题意
有一个 \(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
| #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; }
|