문제
백준 9376
방법 1
설명
경우를 최수 두명이 중간에 만나는지 안 만나는지로 나누었다.
만나지 않는 경우는 다익스트라로 각각 구해서 더하면 된다.
만나는 경우는 위에서 구한 dps[0][]
, dps[1][]
에서 같은 좌표에서의 합이 만나지 않는 경우보다 작은 지점에서 만나서 밖으로 나가야한다. 이때 이 지점은 하나가 아닐 수도 있다. 다익스트라의 초기화 때 이 지점들을 모두 큐에 넣어, 이러한 모든 지점부터의 바깥까지의 최단 거리를 구하자. 이때 정점은 pos
가 되고 가중치는 dps[0][pos] + dps[1][pos]
가 된다. 단 넣는 지점이 '#'
이면 값이 중복되므로 1을 빼주어야 한다.
0-1 너비 우산 탐색
간선이 0~1 뿐이므로 다익스트라에서 priority_queue
를 쓰지 않고 선형 시간에 최단정점을 구하는 0-1 너비 우선 탐색 을 사용할 수 있다. deque
을 사용해 추가되는 weight 가 0 이면 push_front()
를 하고 1이면 push_back()
을 하면 weight 상의 정렬이 유지가 되어 다익스트라의 시간복잡도가 \(\mathrm{O}(\vert V \vert + \vert E \vert)\) 가 된다.
이 기법을 사용하면서 위의 설명대로 여러정점을 넣을 때 정렬을 꼭 적용하자. 우선순위 큐가 아니라서 자동으로 정렬되지 않기 때문이다. 안해도 시간초과가 날 정도로 느려지진 않지먄 40ms -> 16ms 로 꽤 차이가 난다.
시간 복잡도
\(\mathrm{O}(\vert V \vert+ \vert E \vert\))
코드
const int MAX_IN = 101 * 101 ;
const int INF = 1e9 ;
char board [ MAX_IN ];
int dps [ 2 ][ MAX_IN ];
int h , w ;
inline bool Valid ( int x , int y ) { return x >= 0 && y >= 0 && x < w && y < h ; }
inline bool Out ( int x , int y ) { return x == 0 || y == 0 || x == w - 1 || y == h - 1 ; }
int xd [] = { - 1 , 1 , 0 , 0 };
int yd [] = { 0 , 0 , - 1 , 1 };
int Dijkstra ( const vector < pair < int , int >>& starts , int * dp )
{
int ans = 1e9 ;
fill ( dp , dp + w * h , INF );
deque < pair < int , int >> q ;
for ( auto & s : starts )
{
q . push_back ({ - s . first , s . second });
dp [ s . second ] = s . first ;
}
while ( ! q . empty ())
{
auto cur = q . front (); q . pop_front ();
if ( dp [ cur . second ] < cur . first ) continue ; // out of updated edge
int x , y ;
x = cur . second % w ; y = cur . second / w ;
if ( Out ( x , y ))
if ( ans > dp [ cur . second ])
ans = dp [ cur . second ];
for ( int i = 0 ; i < 4 ; i ++ )
{
if ( ! Valid ( x + xd [ i ], y + yd [ i ])) continue ;
int d = ( y + yd [ i ]) * w + x + xd [ i ];
if ( board [ d ] == '*' ) continue ;
int w = board [ d ] == '.' ? 0 : 1 ;
if ( dp [ cur . second ] + w < dp [ d ])
{
dp [ d ] = dp [ cur . second ] + w ;
w ? q . push_back ({ dp [ d ], d }) : q . push_front ({ dp [ d ], d });
}
}
}
return ans ;
}
int main ()
{
int T ;
cin >> T ;
while ( T -- )
{
vector < int > pris ; int ans = INF ;
cin >> h >> w ;
for ( int i = 0 ; i < w * h ; i ++ )
{
cin >> board [ i ];
if ( board [ i ] == '$' ) {
pris . push_back ( i );
board [ i ] = '.' ;
}
}
// 겹치지 않는 경우
ans = Dijkstra ({ { 0 , pris [ 0 ]} }, dps [ 0 ]) + Dijkstra ({ { 0 , pris [ 1 ]} }, dps [ 1 ]);
// 겹치는 경우
vector < pair < int , int >> pts ;
for ( int i = 0 ; i < w * h ; i ++ )
{
int cost = dps [ 0 ][ i ] + dps [ 1 ][ i ] + ( board [ i ] == '#' ? - 1 : 0 );
if ( dps [ 0 ][ i ] != INF && cost < ans )
pts . push_back ({ cost , i });
}
sort ( pts . begin (), pts . end ()); // 이거 없으면 느려짐
ans = min ( ans , Dijkstra ( pts , dps [ 0 ]));
cout << ans << '\n' ;
}
}
방법 2
설명
모든 지점에서 바깥까지의 거리 + 죄수1까지의 거리 + 죄수2까지의 거리
를 구한다. 선택된 지점 이전에는 최수가 만나지 않았다고 가정한다. 먄약 선택된 지점으로 가는 도중에 죄수가 만난다면 문을 연 횟수가 중복되어 더해지므로 최솟값이 될 수 없어 상관없다.
바깥까지의 거리를 구하기 위해서 바깥에 빈공간을 한겹 만들어서 처리한다. 자세한건 여기 참고.
이 방법이 비교적 더 빠르고, 0-1 너비 우선 탐색을 사용하지 않으면 2배 이상 더 빨랐다. 방법 1과 하는 작업은 크게 다르지 않지만 정점을 구해서 옮기는 과정에서 오버헤드가 있기 때문으로 보인다.
시간 복잡도
\(\mathrm{O}(\vert V \vert+ \vert E \vert\))
코드
const int MAX_IN = 103 * 103 ;
const int INF = 1e9 ;
char board [ MAX_IN ];
int dps [ 3 ][ MAX_IN ];
int h , w ;
inline bool Valid ( int x , int y ) { return x >= 0 && y >= 0 && x < w && y < h ; }
inline bool Out ( int x , int y ) { return x == 0 || y == 0 || x == w - 1 || y == h - 1 ; }
int xd [] = { - 1 , 1 , 0 , 0 };
int yd [] = { 0 , 0 , - 1 , 1 };
void Dijkstra ( const vector < pair < int , int >>& starts , int dest , int * dp )
{
fill ( dp , dp + w * h , INF );
deque < pair < int , int >> q ;
for ( auto & s : starts )
{
q . push_back ({ - s . first , s . second });
dp [ s . second ] = s . first ;
}
while ( ! q . empty ())
{
auto cur = q . front (); q . pop_front ();
if ( dp [ cur . second ] < cur . first ) continue ; // out of updated edge
int x , y ;
x = cur . second % w ; y = cur . second / w ;
for ( int i = 0 ; i < 4 ; i ++ )
{
if ( ! Valid ( x + xd [ i ], y + yd [ i ])) continue ;
int d = ( y + yd [ i ]) * w + x + xd [ i ];
if ( board [ d ] == '*' ) continue ;
int w = board [ d ] == '.' ? 0 : 1 ;
if ( dp [ cur . second ] + w < dp [ d ])
{
dp [ d ] = dp [ cur . second ] + w ;
w ? q . push_back ({ dp [ d ], d }) : q . push_front ({ dp [ d ], d });
}
}
}
}
int main ()
{
fastio ;
int T ;
cin >> T ;
while ( T -- )
{
vector < int > pris ; int ans = INF ;
cin >> h >> w ;
h += 2 ; w += 2 ;
fill ( board , board + w * h , '.' );
for ( int i = 1 ; i < h - 1 ; i ++ )
for ( int j = 1 ; j < w - 1 ; j ++ )
{
int cur = i * w + j ;
cin >> board [ cur ];
if ( board [ cur ] == '$' ) {
pris . push_back ( cur );
board [ cur ] = '.' ;
}
}
Dijkstra ({ { 0 , pris [ 0 ]} }, - 1 , dps [ 0 ]);
Dijkstra ({ { 0 , pris [ 1 ]} }, - 1 , dps [ 1 ]);
Dijkstra ({ { 0 , 0 } }, - 1 , dps [ 2 ]);
vector < pair < int , int >> pts ;
for ( int i = 0 ; i < w * h ; i ++ )
if ( dps [ 0 ][ i ] != INF && dps [ 1 ][ i ] != INF && dps [ 2 ][ i ] != INF )
{
int cost = dps [ 0 ][ i ] + dps [ 1 ][ i ] + dps [ 2 ][ i ] + ( board [ i ] == '#' ? - 2 : 0 );
ans = min ( cost , ans );
}
cout << ans << '\n' ;
}
}
댓글남기기