题目大意:给出n*m的区域各个矩形内地形的高度,向这块区域倒无穷多的水,直到水面不再变化,求各处水面的深度。
第一想法是多次以某一个格子高度为基准bfs尝试水是否会溢出逐步提升各个格子的水面高度,但是明显会TLE。然后逐步优化,既然是多次BFS为何不改成spfa呢?显然如果要用spfa就要修改数学模型,不能再一个一个的尝试,而是要一次搜索将所有的格点都修改。
首先我们可以确定的是一个点的水的高度(相对于0)实际上就是从边界到该点的路径上的最高值的最小化的结果。
如图所示:
由于一个水池的边框一定是一个封闭图形,而从整个矩形的边界到达封闭图形的内部的路径上一定会经过封闭图形的边框,最小化路径上的最大高度的就是其边框上的最小值即限制水面高度的缺口。
简略地证明一下:若这个缺口不是最小化的路径上最大高度值,若它不是最小的那个最大值,那么一定就有比它高度更小的水池缺口,而是其他方块限制了这个水池的高度。当然由于这个是网格图不加slf优化的spfa不是好spfa。
代码:
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#define maxn 1010
#define inf (~0u>>1)
#define maxm 1010010
#define lim 6010010
using namespace std;
inline void read(int &x){
char ch;
bool flag=false;
for (ch=getchar();!isdigit(ch);ch=getchar())if (ch=='-') flag=true;
for (x=0;isdigit(ch);x=x*10+ch-'0',ch=getchar());
x=flag?-x:x;
}
inline void write(int x){
static const int maxlen=100;
static char s[maxlen];
if (x<0) { putchar('-'); x=-x;}
if(!x){ putchar('0'); return; }
int len=0; for(;x;x/=10) s[len++]=x % 10+'0';
for(int i=len-1;i>=0;--i) putchar(s[i]);
}//读入输出优化
int map[maxn][maxn];
int f[maxn][maxn];
bool vis[maxn][maxn];
bool bo[maxn][maxn];
int n,m;
bool bound(int x,int y){
if ((x==1)or(y==1)or(x==n)or(y==m))
return 1;
return 0;
}
bool check(int x,int y){
if ((x<1)or(x>n)or(y<1)or(y>m))
return 0;
return 1;
}
const int go[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
int d[maxm*7][2],t,w;
void spfa(){
t=0; w=0;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
if (bound(i,j))
{
++w;
d[w][0]=i; d[w][1]=j;
f[i][j]=map[i][j];
vis[i][j]=1;
}
while (t!=w)
{
++t;
if (t>lim)
t=1;
int x=d[t][0],y=d[t][1];
for (int i=0;i<4;i++)
{
if (check(x+go[i][0],y+go[i][1]))
{
int gox=x+go[i][0],goy=y+go[i][1];
if (f[gox][goy]>f[x][y])
{
f[gox][goy]=max(f[x][y],map[gox][goy]);
if (!vis[gox][goy])
{
++w;
if (w>lim)
w=1;
d[w][0]=gox; d[w][1]=goy;
if (f[d[w][0]][d[w][1]]<f[d[t+1][0]][d[t+1][1]])
{
swap(d[w][0],d[t+1][0]);
swap(d[w][1],d[t+1][1]);
}//slf优化
vis[gox][goy]=1;
}
}
}
}
vis[x][y]=0;
}
}
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
{
read(map[i][j]);
f[i][j]=inf;
}
spfa();
for (int i=1;i<=n;i++)
{
for (int j=1;j<m;j++)
{
write(f[i][j]-map[i][j]);
printf(" ");
}
printf("%d\n",f[i][m]-map[i][m]);
}
return 0;
}
但是但是上面只是bfs的一种优化,既然我们可以改多次尝试为多次更新,我们为何不尝试把多次更新改为一次更新一大块呢?这就要用到并查集的思想了,把多个点的高度绑定在一起作为一个整体同时更新。把所有高度的柱子从低到高排序,依次枚举,如果当前柱子相邻的柱子里有水平面比其更低或相等的柱子且这个水平面还没和边界相连,那么就把这两个水平面联结为一个水平面高度,如果这个邻柱和边界相连,则这个柱子也标记为边界。
代码如下:
#include <cmath>
#include <queue>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define maxn 1010
#define maxm 1010001
#define ll long long
using namespace std;
inline void read(int &x){
char ch;
bool flag=false;
for (ch=getchar();!isdigit(ch);ch=getchar())if (ch=='-') flag=true;
for (x=0;isdigit(ch);x=x*10+ch-'0',ch=getchar());
x=flag?-x:x;
}
inline void write(int x){
static const int maxlen=100;
static char s[maxlen];
if (x<0) { putchar('-'); x=-x;}
if(!x){ putchar('0'); return; }
int len=0; for(;x;x/=10) s[len++]=x % 10+'0';
for(int i=len-1;i>=0;--i) putchar(s[i]);
}
int fa[maxm];
int n,m;
int id[maxm];
int map[maxn][maxn];
bool bo[maxn][maxn];
bool cmp(int a,int b){
return map[a/m][a%m]<map[b/m][b%m];
}
bool bound(int x,int y){
if ((x==0)or(y==0)or(x==n-1)or(y==m-1))
return 1;
return 0;
}
bool check(int x,int y){
if ((x<0)or(x>=n)or(y<0)or(y>=m))
return 0;
return 1;
}
void input(){
scanf("%d%d",&n,&m);
for (int i=0;i<n;i++)
for (int j=0;j<m;j++)
{
read(map[i][j]);
bo[i][j]=bound(i,j);
}
for (int i=0;i<n*m;i++)
{
id[i]=i;
fa[i]=i;
}
}
int find(int x){
if (fa[x]==x)
return x;
fa[x]=find(fa[x]);
return fa[x];
}
const int go[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
int ans[maxn][maxn];
int main(){
input();
sort(id,id+m*n,cmp);
for (int i=0;i<m*n;i++)
{
int tmp=id[i];
int x=tmp/m,y=tmp%m;
for (int j=0;j<4;j++)
if (check(x+go[j][0],y+go[j][1]))
{
int gox=x+go[j][0],goy=y+go[j][1];
if (map[find(gox*m+goy)/m][find(gox*m+goy)%m]<=map[x][y])
if (!bo[find(gox*m+goy)/m][find(gox*m+goy)%m])
{
if (fa[id[i]]!=fa[gox*m+goy])
fa[find(gox*m+goy)]=find(id[i]);
}
else
bo[x][y]=1;
}
}
for (int i=0;i<n*m;i++)
ans[i/m][i%m]=map[find(i)/m][find(i)%m]-map[i/m][i%m];
for (int i=0;i<n;i++)
{
for (int j=0;j<m-1;j++)
{
write(ans[i][j]);
printf(" ");
}
printf("%d\n",ans[i][m-1]);
}
return 0;
}
这两个不同的算法从两个不同的方面来考虑同一个问题,一个从上向下更新,另一个从下往上更新,一个利用图论的spfa算法的思想来更新提高效率,另一个通过并查集将相邻的两块区域连接在一起以减少更新次数。但就从效率上来看这二者的所花费的时间相差无几,其效率的瓶颈和最原始的bfs一样都是更新的次数,如果要再次提升效率,估计还是要从这个方向上去考虑。