P1606 [USACO07FEB]Lilypad Pond G

P1606 [USACO07FEB]Lilypad Pond G

分析

这题初看,就能想到是求最短路计数。但是坑很多,所以我们换一个角度,我会首先将我的错误思路说一遍,再从中分析错误点,最后给出正解。

(我看到的时候,就觉得很简单,还好奇为什么是紫题,-_-,属实是小丑了)

错解

我们用dist[i] [j]表示以(i,j)结尾的路径中莲花的个数。f[i] [j]表示以(i,j)结尾的所有路径中最短路的路径个数

所以我们只需要,在从起点搜的时候,将边分为两类

  • 一类是向水连边,即需要放置一朵莲花,边权为1
  • 另一类是向除了岩石之外的其他点连边,边权为0

不难想到用双端队列广搜去进行优化。

结束了……吗?

我们再来重新审视一下

错误分析

其实问题出在这f[i] [j]的定义上,来让我们仔细看我们的定义

以(i,j)结尾的所有路径中最短路的路径个数

但是题目的问题是什么?

以(i,j)结尾的所有路径中最少摆放的荷花数的方案

这里,我们来看,其实我们的最短路dist定义是没有错的,就是最少摆放的荷花数。

但是当我们重新审视我们的搜索过程的时候,就会发现错误了。

因为,荷花可能同样的摆放方式,但是到(i,j)的时候走的是不同的0权道路

例如看下边的例子,我们从起点到终点的最短路径有两条,但是实际上摆放荷花的方案只有1

3 2 2
2 2 2
2 0 2
2 2 2
1 2 1
2 2 2
2 1 2
2 2 2
4 2 2

那解决方案是什么呢?

我们仔细观察后可以发现,我们在荷花之间的转移时不能被多次计算的,即一个联通块的荷花应该被计算一次

但这个联通块,我们需要重新定义一下

表示之间能够通过日字形走法所连接起来的联通块

这样,我们就解决了问题。

而这个建图方式也是蛮有意思的,不妨就着重也讲一下

将联通块当成一个独立的点,重新建图

如何建图呢?

我们采用递归的方式,代码是最好的解说方法,请直接看代码

for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            if(g[i][j]==0||g[i][j]==3)//从起点(3)和每一个水(0)开始进行dfs
            {
                memset(st,0,sizeof st);//进去前将需要用到的判重数组清0
                dfs(id[i][j],i,j);
            }

void dfs(int u,int x,int y)
{
    if(st[id[x][y]]) return ;//若是该边已经被经过,直接返回
    st[id[x][y]] = 1;//若没有,则标记为true
    for(int i=0;i<8;i++)
    {
        int nx = x + dx[i],ny = y + dy[i];
        if(nx<0||nx>=n||ny<0||ny>=m||st[id[nx][ny]]) continue;
        if(g[nx][ny]==1) dfs(u,nx,ny);//若是该点是荷花(1),则直接进行递归,
        else if(g[nx][ny]!=2)//若是除2之外的点,则将该点向这个点连一条边
        {
            st[id[nx][ny]]=1;//标记这个点已经被扫过
            add(u,id[nx][ny]);
        }
    }
}

如果不明白,我们可以再看下边的这个状态

3 2 2
2 2 2
2 0 2
2 2 2
1 2 1
2 2 2
2 1 2
2 2 2
4 2 2

其中的三个荷花就属于是一个联通块的,此时从这个联通块,就会把其当成一个点

再(2,1)的0向该联通块所能连接到的所有点连接一条边。

这样我们就避免的荷花之间的转移带来的多余路径。

其中有一些小问题需要注意

  1. 由于是从一个点0/3向荷花的联通块所能到达的所有点连边,因此边数会很大,不如我们就直接设置为N^4,避免麻烦
  2. 由于我们重新建图的时候,二维的坐标不容易建图,我们可以进行调整,将二维坐标变为一维的标号

AC_code

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 35*35,M = N*50,INF = 0x3f3f3f3f;//边数需要开的大一些,因为从当前点,是向一个莲叶的连通块周边的所有点连边,不好估计,推荐直接弄一个N^4
int h[N],e[M],ne[M],idx;
int id[35][35],g[35][35];
int dist[N];
LL f[N];
bool st[N];
int dx[8] = {-2,-1,1,2,2,1,-1,-2},dy[8] = {1,2,2,1,-1,-2,-2,-1};
int S,E;
int n,m;

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void dfs(int u,int x,int y)
{
    if(st[id[x][y]]) return ;//若是该边已经被经过,直接返回
    st[id[x][y]] = 1;//若没有,则标记为true
    for(int i=0;i<8;i++)
    {
        int nx = x + dx[i],ny = y + dy[i];
        if(nx<0||nx>=n||ny<0||ny>=m||st[id[nx][ny]]) continue;
        if(g[nx][ny]==1) dfs(u,nx,ny);//若是该点是荷花(1),则直接进行递归,
        else if(g[nx][ny]!=2)//若是除2之外的点,则将该点向这个点连一条边
        {
            st[id[nx][ny]]=1;//标记这个点已经被扫过
            add(u,id[nx][ny]);
        }
    }
}

void spfa()
{
    queue<int> q;
    q.push(S);
    memset(dist,0x3f,sizeof dist);
    memset(st,0,sizeof st);
    f[S] = 1,dist[S] = 0;
    st[S] = 1;
    while(q.size())
    {
        auto t = q.front();
        q.pop();
        st[t] = 0;
        for(int i=h[t];~i;i=ne[i])
        {
            int j = e[i];
            if(dist[j]>dist[t]+1)
            {
                dist[j] = dist[t] + 1;
                f[j] = f[t];
                if(!st[j])
                {
                    st[j] = 1;
                    q.push(j);
                }
            }
            else if(dist[j]==dist[t]+1)
                f[j] += f[t];
        }
    }
    if(dist[E]==INF)  puts("-1");
    else cout<<dist[E]-1<<endl<<f[E]<<endl;
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(int i=0,k=0;i<n;i++)
        for(int j=0;j<m;j++,k++){
            cin>>g[i][j],id[i][j]=k;
            if(g[i][j]==3) S = k;
            else if(g[i][j]==4) E = k;
        }
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            if(g[i][j]==0||g[i][j]==3)//从起点(3)和每一个水(0)开始进行dfs
            {
                memset(st,0,sizeof st);//进去前将需要用到的判重数组清0
                dfs(id[i][j],i,j);
            }
    spfa();
    return 0;
}
posted @ 2022-03-14 20:12  艾特玖  阅读(173)  评论(0)    收藏  举报