Luogu P3684 Solution / 论如何证明这道题是绿题
闲话
P3684=前缀和+二分+最小生成树+并查集+启发式合并。
本题解为离线做法。
思路
Part 1. 最大通过尺寸预处理
首先,一个集装箱的中心想要通过某个点,则这个点周围必然有足够这个集装箱通过的空间。
所以,我们可以想到对于仓库中的每个点求出能够使其中心通过该点的集装箱的最大尺寸。
该部分的解法为前缀和+二分。
具体来说,我们定义 \(p_{i,j}\) 表示仓库中 \([1,1]\) 位置到 \([i,j]\) 位置中有多少个位置是 .
。
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> ch;
conn[i][j] = (ch == '.') + conn[i - 1][j] + conn[i][j - 1] - conn[i - 1][j - 1];
}
}
接下来,我们就可以在 \(O(1)\) 的时间复杂度内查询一个以 \([x,y]\) 为中心且边长为 \(2r-1\) 的正方形中包含多少个 .
。
inline int sm(int x, int y, int r)
{
return conn[x + r][y + r] - conn[x - r - 1][y + r] - conn[x + r][y - r - 1] + conn[x - r - 1][y - r - 1];
}
接下来,由于在同一个位置放不同尺寸的集装箱的可行性具有单调性,我们就可以在 \(O(n^2\log n)\) 的时间复杂度内算出仓库中每个点能够允许通过集装箱的最大尺寸。
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
a = 1, b = min(min(i, n - i + 1), min(j, n - j + 1));
while (a <= b)
{
mid = (a + b) >> 1;
if (sm(i, j, mid - 1) == (mid * 2 - 1) * (mid * 2 - 1))
a = mid + 1, rs[cid(i, j)] = mid;
else
b = mid - 1;
}
prs[cid(i, j)] = cid(i, j);
}
}
这里的 cid(i,j)
函数是一个将二维坐标映射到一维坐标上的函数,读者自行实现不难。
Part 2. 求解
如果一个尺寸为 \(r\) 的集装箱可以从坐标 \((x_1,y_1)\) 运送到 \((x_2,y_2)\),那么 \((x_1,y_1)\) 和 \((x_2,y_2)\) 之间必然存在一条路径,使得该路径上经过所有的尺寸上的位置允许通过集装箱的最大尺寸将均不小于 \(r\)。
由此,我们可以得出两点间最大通过尺寸的贪心求法:按照能通过的最大尺寸的降序顺序激活仓库上的各个位置,一旦两点连通,这两点中最大通过尺寸就是连通前激活的最后一个位置允许集装箱通过的最大尺寸。而这就是 Kruskal 最小生成树的思想。
sort(prs + 1, prs + n * n + 1, [&](int x, int y)
{
if(rs[x]!=rs[y])return rs[x]>rs[y];
return x<y; });
然而,如果这道题要求在线,那么每次询问时都需要清空连通信息重新建图,使得时间复杂度无法接受。
但问题是,它不要求在线。
上面提到,一旦两点连通,这两点中最大通过尺寸就是连通前激活的最后一个位置允许集装箱通过的最大尺寸。也就是说,我们可以在开始时将询问的信息挂在仓库对应的位置上。一旦进行合并操作时发现有一个询问同时处于两个连通块上,那么答案就是目前访问到位置的最大允许通过尺寸。
for (int i = 1; i <= m; i++)
{
read(a, b, c, d);
st[cid(a, b)].insert(i);
st[cid(c, d)].insert(i);
}
合并连通块信息显然可以通过并查集,set
和启发式合并解决。
inline void merge(int x, int y)
{
x = find(x), y = find(y);
if (x == y)
return;
f[y] = x;
if (st[x].size() < st[y].size())
st[x].swap(st[y]);
for (auto &i : st[y])
{
if (st[x].count(i))
ans[i] = cr, st[x].erase(i);
else
st[x].insert(i);
}
st[y].clear();
}
在完成了上述所有步骤后,问题就解决了。
时间复杂度
- 输入 \(O(n^2+q)\)
- 建图+求解最大通过尺寸 \(O(n^2+n^2\log n)=O(n^2\log n)\)
- 按最大通过尺寸排序位置 \(O(n^2\log n^2)\)
- 询问挂载 \(O(q\log q)\)
- 按顺序激活位置+求解 \(O(n^2\alpha(n^2)+n^2\log q)=O(n^2\log q)\)
总时间复杂度为 \(O(n^2\log n^2)\)。
完整代码
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
const int N = 1e3 + 10, M = 3e5 + 10;
template <typename _Tp>
inline void read(_Tp &x)
{
char ch;
while (ch = getchar(), !isdigit(ch))
;
x = ch - '0';
while (ch = getchar(), isdigit(ch))
x = x * 10 + ch - '0';
}
template <typename _Tp, typename... _Ano>
inline void read(_Tp &x, _Ano &...ano)
{
read(x);
read(ano...);
}
int n, conn[N][N], f[N * N], m, a, b, c, d, prs[N * N], rs[N * N], mid, cr, cp, ans[M];
set<int> st[N * N];
inline int cid(int x, int y)
{
return (x - 1) * n + y;
}
inline int sm(int x, int y, int r)
{
return conn[x + r][y + r] - conn[x - r - 1][y + r] - conn[x + r][y - r - 1] + conn[x - r - 1][y - r - 1];
}
inline int find(int x)
{
return x == f[x] ? x : f[x] = find(f[x]);
}
inline void merge(int x, int y)
{
x = find(x), y = find(y);
if (x == y)
return;
f[y] = x;
if (st[x].size() < st[y].size())
st[x].swap(st[y]);
for (auto &i : st[y])
{
if (st[x].count(i))
ans[i] = cr, st[x].erase(i);
else
st[x].insert(i);
}
st[y].clear();
}
char ch;
int main()
{
read(n);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> ch;
conn[i][j] = (ch == '.') + conn[i - 1][j] + conn[i][j - 1] - conn[i - 1][j - 1];
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
a = 1, b = min(min(i, n - i + 1), min(j, n - j + 1));
while (a <= b)
{
mid = (a + b) >> 1;
if (sm(i, j, mid - 1) == (mid * 2 - 1) * (mid * 2 - 1))
a = mid + 1, rs[cid(i, j)] = mid;
else
b = mid - 1;
}
prs[cid(i, j)] = cid(i, j);
}
}
sort(prs + 1, prs + n * n + 1, [&](int x, int y)
{
if(rs[x]!=rs[y])return rs[x]>rs[y];
return x<y; });
read(m);
for (int i = 1; i <= m; i++)
{
read(a, b, c, d);
st[cid(a, b)].insert(i);
st[cid(c, d)].insert(i);
}
for (int i = 1; i <= n * n; i++)
{
cp = prs[i];
if (!rs[cp])
break;
cr = rs[cp] * 2 - 1;
f[cp] = cp;
if (cp > n and f[cp - n])
merge(cp, cp - n);
if (cp + n <= n * n and f[cp + n])
merge(cp, cp + n);
if (cp % n and f[cp + 1])
merge(cp, cp + 1);
if (cp % n != 1 and f[cp - 1])
merge(cp, cp - 1);
}
for (int i = 1; i <= m; i++)
{
printf("%d\n", ans[i]);
}
}
结语
综上所述,本题使用的算法包含前缀和、二分搜索、最小生成树、并查集以及启发式合并。不难发现,这些算法本身的难度都在普及/提高-或以下。本题难度最高的地方其实在于想出将询问挂载在节点上进行处理的技巧,而此技巧实际上不会超过普及+/提高难度。
所以,本题难度应为绿。