一道很好的思維題,被教練碾壓了。
觀察
首先從題目給的樣例入手:
5 5
.....
.....
.....
.....
.....
這種情況最終的答案是:
YES
....#
...#.
..#..
.#...
#....
所以我們大膽猜測,構造這樣的仙人掌防線,需要我們斜著且不間斷地排布仙人掌。
這一點其實也可以從題目中 “仙人掌不能四聯通放” “需要以四聯通的形式堵住怪物” 觀察出來,只是初看比較難想到。
因此,我們只要在圖中構造一個斜向走的仙人掌防線即可。
假做法
既然我們要用最少的仙人掌去構造這樣的一個放置方案,就需要利用到之前已經擺放過的仙人掌了。
同時看到我們求的是一個放置的輪廓線,並且是從左往右放的,那麼我們就可以想到經典的按輪廓線轉移的線性 dp 。
當這個位置是仙人掌時,我們不需要將這個位置再放上仙人掌,因此直接轉移前面的:
當這個位置是空的時,我們要在這裡放上仙人掌,因此轉移時要加一:
當這個位置是與某個仙人掌四聯通時,無法放置,因此不轉移。
但是,這個做法實際上是假的,因為我們並不能保證仙人掌防線一定是一直從左往右的,例如:
.......#
......#.
.....#..
....#...
...#....
..#.....
.#......
..#.....
...#....
....#...
...#....
..#.....
.#......
#.......
可以從左上,左下,右上,右下四個地方轉移,這就直接說明了本題不滿足無後效性,不能用 dp 。因此需要換一種思路。
正解
根據上述分析我們知道,轉移可以從左上,左下,右上,右下四個地方來。因此我們可以建出一個圖,表示這個點能從其他什麼點走過來,形成防線。
這樣,就把 dp 變成了 無需滿足無後效性的 圖論問題。
這是經典的把有後效性的 dp 轉化為最短路的 trick 。
複雜度更劣做法
我們把和仙人掌四聯通的點設為不可以走的點,對於一個有向邊 \(<u,v>\) ,如果 \(v\) 是已有仙人掌的點,那麼把這條邊的權設為 \(0\) ,否則設為 \(1\) ,這代表著走這裡需不需要種仙人掌。
這裡由於是無向圖,所以要建兩倍有向邊。
接下來跑一遍 dijkstra 即可,求出的最短路徑記錄一下自己的先驅,然後順著最短路徑把這上面的點修改成仙人掌再輸出就可以了。
時間為 \(O(\sum( nm \log (nm)))\),可以過但是沒有到最優複雜度。
複雜度更優做法
觀察到邊權只有 \(0\) 和 \(1\) ,因此我們可以使用 01 BFS 演算法來在 \(O(n)\) 時間內求解最短路。
01 BFS 板子:Switch the lamp on 。
還要注意的是 01 BFS 只有在節點從 deque 裡出來後才能判斷是否抵達終點,因為如果在 push 時就判斷,則會導致可能這個節點先更新了邊權更大的節點,導致其先到終點就結束,而沒有更新邊權更小的節點導致算錯最短路的情況。
我這裡採用了陣列模擬 deque ,為了減少常數,並且要注意這裡陣列沒有辦法直接開下,因此我們要用 vector 存。
坑點:vector 太大了,必須開在全域性裡,如果開在區域性會爆棧導致 RE 。二維的 vector 在 clear 時要分維 clear ,無法一次 clear 掉全部。
時間為 \(O(\sum( nm ))\)。
01 BFS 程式碼
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pi;
int n,m;
int gox[]={0,0,1,-1};
int goy[]={1,-1,0,0};
int xx[]={1,1,-1,-1};
int yy[]={-1,1,-1,1};
bool check(int x,int y)
{
return (x>=1&&x<=n&&y>=1&&y<=m);
}
pi q[1500005];
vector<int>c[200005];//不能開在區域性
vector<int>f[200005];
vector<pi>pre[200005];
vector<pi>cac;
int h,t;
void outp(int ex,int ey)
{
cout<<"YES"<<endl;
while((ex!=-1)&&(ey!=-1))
{
c[ex][ey]=0;
pi tmp=pre[ex][ey];
ex=tmp.first;
ey=tmp.second;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(c[i][j]==0)cout<<'#';
else cout<<'.';
}
cout<<endl;
}
}
void solve()
{
for(auto tmp:cac)
{
int x=tmp.first,y=tmp.second;
c[x][y]=0;
for(int i=0;i<4;i++)
{
int tx=x+gox[i],ty=y+goy[i];
if(check(tx,ty)&&c[tx][ty]==1)c[tx][ty]=-1;
}
}
h=750000,t=750000;
for(int i=1;i<=n;i++)
{
if(c[i][1]!=-1)
{
if(c[i][1]==0)q[--h]={i,1};
else q[t++]={i,1};
f[i][1]=c[i][1];
pre[i][1]={-1,-1};
}
}
while(t-h>0)
{
pi now=q[h++];
int x=now.first,y=now.second;
int w=f[x][y];
if(y>=m)//這裡不能放在 push 時判斷
{
outp(x,y);
return;
}
for(int i=0;i<4;i++)
{
int nx=xx[i]+x,ny=yy[i]+y;
if(check(nx,ny)&&f[nx][ny]==0x3f3f3f3f&&c[nx][ny]!=-1)
{
int nw=w+c[nx][ny];
f[nx][ny]=nw;
if(t-h<=0||nw<=f[q[h].first][q[h].second])q[--h]={nx,ny};
else q[t++]={nx,ny};
pre[nx][ny]={x,y};
}
}
}
cout<<"NO"<<endl;
}
int main()
{
int t;
cin>>t;
while(t--)
{
for(int i=0;i<=n;i++)//多維 vector 要逐維 clear ,不然會 CE 。
{
c[i].clear();
f[i].clear();
pre[i].clear();
}
cac.clear();
cin>>n>>m;
for(int i=1;i<=n;i++)
{
c[i].push_back(0);
f[i].push_back(0x3f3f3f3f);
pre[i].push_back({0,0});
for(int j=1;j<=m;j++)
{
char tmp;
cin>>tmp;
if(tmp=='#')cac.push_back({i,j});
c[i].push_back(1);
f[i].push_back(0x3f3f3f3f);
pre[i].push_back({0,0});
}
}
solve();
}
return 0;
}