题目描述

scx 来到了一个新的城市旅行。她发现了这个城市的布局是网格状的,也就是有 $n$ 条从东到西的道路和 $m$ 条从南到北的道路,这些道路两两相交形成 $n \times m$ 个路口 $(i, j)$ ($1 \leq i \leq n; 1 \leq j \leq m$)。

她发现不同的道路路况不同,所以通过不同的路口需要不同的时间。通过调查发现,从路口 $(i, j)$ 到路口 $(i, j+1)$ 需要时间 $r_{i, j}$,从路口 $(i, j)$ 到路口 $(i+1, j)$ 需要时间 $c_{i, j}$。注意这里的道路是双向的,也就是从路口 $(i, j + 1)$ 到路口 $(i, j)$ 需要时间同样是 $r_{i, j}$。

scx 有 $q$ 个询问,她想知道从路口 $(x_1, y_1)$ 到路口 $(x_2, y_2)$ 最少需要花多少时间。

输入格式

第一行包含两个正整数 $n, m$ ($n \times m \leq 20000$),表示城市的大小。

接下来 $n$ 行,每行包含 $m - 1$ 个正整数,第 $i$ 行第 $j$ 个正整数表示从一个路口到另一个路口的时间 $r_{i, j}$ ($1 \leq r_{i, j} \leq 10^4$)。

接下来 $n - 1$ 行,每行包含 $m$ 个整数,第 $i$ 行第 $j$ 个正整数表示从一个路口到另一个路口的时间 $c_{i, j}$ ($1 \leq c_{i, j} \leq 10^4$)。

接下来一行,包含一个正整数 $q$ ($q \leq 10^5$),表示 scx 的询问个数。

接下来 $q$ 行,每行包含四个正整数 $x_1, y_1, x_2, y_2$ ($1 \leq x_1, x_2 \leq n; 1 \leq y_1, y_2 \leq m$),表示两个路口的位置。

输出格式

输出共 $q$ 行,每行包含一个整数,表示从一个路口到另一个路口最少需要花的时间。

题解

容易看出这是一个最短路模型。但是对于多组询问怎么办呢?

如果暴力的话,那么时间复杂度至少是 $O \left( q \left| V \right| \right)$ 的 (其中 $\left| V \right| = n \times m$),是跑不过去的。

既然在线算法不好搞,我们来考虑将询问离线。

考虑使用分治,我们用一条线 $l$ 将原网格图一分为二,那么对于两端点在两个不同块中的询问,所有的路径必须经过线 $l$,从而我们可以枚举 $l$ 上的点,使用 Dijkstra (或 SPFA) 跑最短路后将两端结果相加即可。

如果询问的两个端点在同一块,那么分两种情况:

  1. 如果最短路经过线 $l$,则它一定会在这一轮被更新到。
  2. 否则,最短路一定在那个块内,可以发现它已经变成了原问题的一个子问题,因此递归解决即可。

总之,我们要把该轮中所有的询问均用这些答案更新,然后将剩下一些两个端点在同一块的询问分别递归处理。

由于是分治,因此分得一定要尽量均匀,于是我们每次取矩形的一条中线分割。

至于是水平中线还是垂直中线,注意到每次我们要对所有 $l$ 上的点跑 Dijkstra,因此当然要选择较短的中线啦。

接下来来分析一波时间复杂度。

第 $k$ 次分治,顶点数为 $O \left( \dfrac {\left| V \right|} {2^k} \right)$,因此 $l$ 的长度 $|l|$ 不超过 $O \left( \sqrt{ \dfrac {\left| V \right|} {2^k} } \right)$。

由于 Dijkstra 的复杂度为 $O \left( \left| E \right| \log \left| V \right| \right)$,且网格图中 $\left| E \right| = O \left( \left| V \right| \right)$,因此每次 Dijkstra 的复杂度为 $O \left( \dfrac {\left| V \right|} {2^k} \log \dfrac {\left| V \right|} {2^k} \right) = O \left( \dfrac {\left| V \right|} {2^k} \log \left| V \right| \right)$。

由于第 $k$ 层分治一共会出现 $2^k$ 次,因此第 $k$ 层分治对总复杂度的贡献为

$$ O \left( 2^k \cdot \sqrt{ \frac {\left| V \right|} {2^k} } \cdot \frac {\left| V \right|} {2^k} \log \left| V \right| \right) = O \left( \frac {\left| V \right|^{3/2} \log \left| V \right|} {2^{k/2}} \right) $$

对 $k$ 求和,分母由无穷递缩等比数列可知为一常数,故总的时间复杂度即为 $O \left( \left| V \right|^{3/2} \log \left| V \right| \right)$。

代码

#include <bits/stdc++.h>
#define N 100020

const int INF = 0x3f3f3f3f;

struct request {
	int r1, c1, r2, c2;
	request * read() {scanf("%d%d%d%d", &r1, &c1, &r2, &c2); return this;}
} qry[N];

struct edge {
	int u, v, w;
	edge (int u0 = 0, int v0 = 0, int w0 = 0) : u(u0), v(v0), w(w0) {}
} e[N];

int R, C, E, q;

int first[N], next[N];
int ord[N], buf[N * 2];
int ans[N];

inline void down(int &x, const int y) {x > y ? x = y : 0;}
inline int join(int r, int c) {return (r - 1) * C + c;}
inline void split(int id, int &r, int &c) {r = --id / C + 1; c = id % C + 1;}

inline void addedge(int u, int v, int w) {
	e[++E] = edge(u, v, w); next[E] = first[u]; first[u] = E;
	e[++E] = edge(v, u, w); next[E] = first[v]; first[v] = E;
}

namespace SP {
	struct node {
		int to, dist;
		node (int _to = 0, int _dist = 0) : to(_to), dist(_dist) {}
		inline bool operator < (const node &B) const {return dist > B.dist;}
	};

	using PQ = std::priority_queue <node>;

	int minR, maxR, minC, maxC;
	int d[N];
	PQ pq;

	inline void setBound(int rL, int rR, int cL, int cR) {minR = rL; maxR = rR; minC = cL; maxC = cR;}

	void Dijkstra(int s) {
		int i, j, x, y, r, c; node t;
		for (i = minR; i <= maxR; ++i)
			for (j = minC; j <= maxC; ++j)
				d[join(i, j)] = INF;
		d[s] = 0; pq.push(node(s, 0));
		for (; !pq.empty(); ) {
			t = pq.top(); pq.pop();
			if (d[x = t.to] < t.dist) continue;
			for (i = first[x]; i; i = next[i]) {
				split(y = e[i].v, r, c);
				if (minR <= r && r <= maxR && minC <= c && c <= maxC && d[x] + e[i].w < d[y = e[i].v]) {
					d[y] = d[x] + e[i].w;
					pq.push(node(y, d[y]));
				}
			}
		}
	}
}

void Partition(int rL, int rR, int cL, int cR, int *_beg, int *_end) {
	if (rL > rR || cL > cR || _beg == _end) return;
	int u, v, *p, *p1 = buf, *p2 = buf + N, *_scx, *_fy;
	if (rR - rL >= cR - cL) { // row split
		int rM = rL + rR >> 1, c;
		for (c = cL; c <= cR; ++c) {
			SP::setBound(rL, rR, cL, cR);
			SP::Dijkstra(join(rM, c));
			for (p = _beg; p != _end; ++p) {
				u = join(qry[*p].r1, qry[*p].c1);
				v = join(qry[*p].r2, qry[*p].c2);
				down(ans[*p], SP::d[u] + SP::d[v]);
			}
		}
		for (p = _beg; p != _end; ++p)
			if (qry[*p].r1 < rM && qry[*p].r2 < rM) *p1++ = *p;
			else if (qry[*p].r1 > rM && qry[*p].r2 > rM) *p2++ = *p;
		_scx = _beg + (p1 - buf);
		_fy = _scx + (p2 - (buf + N));
		memcpy(_beg, buf, (_scx - _beg) * sizeof(int));
		memcpy(_scx, buf + N, (_fy - _scx) * sizeof(int));
		Partition(rL, rM - 1, cL, cR, _beg, _scx);
		Partition(rM + 1, rR, cL, cR, _scx, _fy);
	} else { // column split
		int cM = cL + cR >> 1, r;
		for (r = rL; r <= rR; ++r) {
			SP::setBound(rL, rR, cL, cR);
			SP::Dijkstra(join(r, cM));
			for (p = _beg; p != _end; ++p) {
				u = join(qry[*p].r1, qry[*p].c1);
				v = join(qry[*p].r2, qry[*p].c2);
				down(ans[*p], SP::d[u] + SP::d[v]);
			}
		}
		for (p = _beg; p != _end; ++p)
			if (qry[*p].c1 < cM && qry[*p].c2 < cM) *p1++ = *p;
			else if (qry[*p].c1 > cM && qry[*p].c2 > cM) *p2++ = *p;
		_scx = _beg + (p1 - buf);
		_fy = _scx + (p2 - (buf + N));
		memcpy(_beg, buf, (_scx - _beg) * sizeof(int));
		memcpy(_scx, buf + N, (_fy - _scx) * sizeof(int));
		Partition(rL, rR, cL, cM - 1, _beg, _scx);
		Partition(rL, rR, cM + 1, cR, _scx, _fy);
	}
}

int main() {
	int i, j, n, w;
	scanf("%d%d", &R, &C);
	for (n = i = 1; i <= R; ++i, ++n)
		for (j = 1; j < C; ++j, ++n)
			scanf("%d", &w), addedge(n, n + 1, w);
	for (n = i = 1; i < R; ++i)
		for (j = 1; j <= C; ++j, ++n)
			scanf("%d", &w), addedge(n, n + C, w);
	scanf("%d", &q);
	for (i = 0; i < q; ++i) qry[i].read(), ord[i] = i;
	memset(ans, 63, q * sizeof(int));
	Partition(1, R, 1, C, ord, ord + q);
	for (i = 0; i < q; ++i) printf("%d\n", ans[i]);
	return 0;
}

坑1:要注意即使询问的两个端点在同一块,也要更新答案,因为有可能最短路就是要跨过线 $l$ 的。

坑2:Dijkstra 注意只需跑到分治的边界,跑出去复杂度就不能保证了。

坑3:__gnu_pbdspriority_queue 简直就是个废物,用它被卡常了,还是用 std::priority_queue 保平安……