1. 程式人生 > >JZOJ100048 【NOIP2017提高A組模擬7.14】緊急撤離

JZOJ100048 【NOIP2017提高A組模擬7.14】緊急撤離

題目

在這裡插入圖片描述

題目大意

給你一個01矩陣,每次詢問從一個點是否可以走到另一個點。
每次走只能往右或者往下。


思考歷程

這題啊,我想的時候真的是腦洞大開……
首先,我一眼看下去,既然要詢問是否聯通,那麼能不能求出它們的最短路,看看是不是它們的曼哈頓距離?
看到資料範圍之後這個想法徹底涼涼……
然後就開始考慮一些正經的方法……
首先,考慮如何掃描線……類似掃描線的,掃一掃,維護一下,說不定就可以了呢?
然後,我發現無論如何,我都難以逃脫 O (

n 2 m 2 ) O(n^2m^2) ,就算是使用bitset也不行。
這樣不行啊,我就考慮分治?
如何分治?
每次在中間的這一條線上開始,向兩邊進行轉移。轉移什麼?轉移每個點到這條線上連通的狀態……
bitset
可以優化一下。
但是又感覺,這個方法好像過不去,所以,我又繼續向其他的做法。
然後就想到了分塊!
如何分塊呢?設塊的行數為 K K ,那麼我們在每個塊的邊界那裡,搞一搞類似上面的轉移。
然後我們在所有的邊界之間,求出它們的連通性。
推了一波複雜度,好想很優秀!
好開心好開心……
然後打了幾行,突然發現:時間複雜度好像算錯了!
非常不爽,又算了一遍。發現還是算錯了,再算一遍……
什麼,這麼慢,連分治都不如?
又看看時間,似乎不多了……
我絕望地打了個暴力,用bitset
隨便優化了一下……


正解

其實正解在比賽時已經想到了。
只不過覺得過不了……
這題的正解就是分治,和上面說的一模一樣!
非常不爽……
這次說詳細一些:
按行或列分治(其實應該按列分治,具體原因……),下面一行為準。
我們將矩陣分成上下兩個部分。
對於上面,我們設 f i , j f_{i,j} 表示點 ( i , j ) (i,j) 到中間的這一行上每個點的狀態。
對於下面,我們設 g i , j g_{i,j} 表示中間這一行上的每個點到 ( i , j ) (i,j) 的狀態。
其實兩個是相反的,具體怎麼轉移顯然。
那麼對於詢問的兩個點,如果它們之間的路徑上會經過這一行,那就列舉經過行上的哪一個點,計算一下是否聯通就好了。至於沒有經過的,直接遞迴分治下去。

然後分析一下時間複雜度。
首先我們知道,很顯然的,分治只有 lg n \lg n 層。
對於每一層,我們需要處理 n m 2 nm^2 次,因為我們考慮同一列上的點,它們轉移所耗費的時間為 n m nm ,即為整個平面。由於每一行有 m m 個東西,所以就是 n m 2 nm^2 次。
綜上,轉移的總時間是 O ( n m 2 lg n ) O(nm^2\lg n)
然後就是處理詢問的時間。
我們首先將詢問全部列在一起,然後在處理的時候,將左邊的區間放左邊,將右邊的區間放右邊,穿過中間行的區間直接處理。時間複雜度可以這麼理解:對於每一個詢問,它相當於在這棵分治所形成的的二叉樹上面往下走,那麼每個的時間為 O ( lg n ) O(\lg n) 。然後我們一共有 q q 個詢問,所以時間複雜度為 O ( q lg n ) O(q\lg n) 。還有每次處理一個詢問都需要 O ( q m ) O(qm) 的時間
為什麼時間複雜度好像和題解不一樣,難道是我分析錯了?還是 lg n \lg n 太小以至於題解不屑於注意?
綜上所述,時間複雜度是 O ( n m 2 lg n + q ( m + lg n ) ) O(nm^2\lg n+q(m+\lg n))
這個時間複雜度似乎過不去,然而,由於有bitset優化,會快很多。
這題的正解就是這麼簡單……


資料上的問題

我要吐槽一下,這題的資料太可惡了!
我打出來之後,發現自己TLE!怎麼可能?
然後,就是瘋狂的卡常數歷程……最終以990+的好時間卡了過去。
我不禁深思,為什麼我的程式這麼慢?我是不是該重修卡常技能?
看看別人的程式,似乎沒有什麼特別的地方啊!
在我絕望之際,忽然,我發現了驚天的祕密!
為什麼他們是按列分治的?我翻遍所有的程式,發現AC的都是按列分治的。有一個按行分治的人過了,但開了O2。
按理來說,按列分治和按行分治的時間複雜度是一樣的。因為它們本質上都是同一個道理。
並且,在列舉的過程中,按行分治和按列分治在常數上的差異其實不大。
(我們可以想一想,不管是按行分治還是按列分治,都是將矩陣分為兩個部分。這兩個部分都是一個矩形,從右下角列舉到左上角,所以說常熟還是差不多的。卡過常數的人們都知道,在列舉的過程中,儘量一個接一個地列舉,不要跳著來。可問題是,都是一個接一個地列舉啊!)
所以說,原因只能歸結於資料。
資料害死人!!!
然後我就腦補了一下出資料的場景:

出題人:這題要卡常才能過!所以我的資料要出大一些!
然後出了各種大資料,將標程可以過的留下來……

然而標程是按列分治的,並沒有按行分治的,所以按行分治的不一定能過……
這隻能怪出題人了。


另外的吐槽

我還發現,這題可以鍛鍊我的卡常技巧!
如何在卡常的情況下,依然能保持程式的美觀?
~~眾所周知,~~我的程式是很美觀的……
然後請看看我的程式碼。


程式碼

這程式碼可能有點神仙,畢竟,美觀與常數不可兼得!

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 500
#define Q 600000
struct bitset{/手打bitset
	unsigned long long b[8];
	inline void reset(){//清空
		memset(b,0,sizeof b);
	}
	inline bool operator[](int y){//查詢某一個位上的值
		return b[y>>6]>>(y&63)&1;
	}
	inline void set(int y){
		b[y>>6]|=1ll<<(y&63);//將某一個位上的值的值賦為1
	}
	inline void getor(bitset *ano){//將當前的bitset和其它bitset取or賦給自己
		b[0]|=ano->b[0];
		b[1]|=ano->b[1];
		b[2]|=ano->b[2];
		b[3]|=ano->b[3];
		b[4]|=ano->b[4];
		b[5]|=ano->b[5];
		b[6]|=ano->b[6];
		b[7]|=ano->b[7];
	}
	inline void get(bitset *x,bitset *y){//將自己的值賦為兩個bitset的or值
		b[0]=x->b[0]|y->b[0];
		b[1]=x->b[1]|y->b[1];
		b[2]=x->b[2]|y->b[2];
		b[3]=x->b[3]|y->b[3];
		b[4]=x->b[4]|y->b[4];
		b[5]=x->b[5]|y->b[5];
		b[6]=x->b[6]|y->b[6];
		b[7]=x->b[7]|y->b[7];
	}
} bs[N*N+1];
int cnt;
inline int input(){//讀入優化
	char ch=getchar();
	while (ch<'0' || '9'<ch)
		ch=getchar();
	int res=0;
	do{
		res=res*10+ch-'0';
		ch=getchar();
	}
	while ('0'<=ch && ch<='9');
	return res;
}
int n,m;
char mat[N+1][N+1];
int q;
struct Question{
	int a,b,c,d;
	int num;
} _t[Q+1],_tmp[Q+1],*t,*tmp;
bitset *f[N][N],*g[N][N];//為什麼用指標呢?因為我們很容易發現,有時只會從一個轉移,那麼指標就可以大大地加快速度。具體見下面。
void dfs(int,int,int,int);
bool ans[Q+1];
int main(){
	n=input(),m=input();
	for (int i=0;i<n;++i)
		scanf("%s",mat[i]);
	q=input();
	int tmpq=q;
	q=0;
	t=_t,tmp=_tmp;
	for (int i=1;i<=tmpq;++i){
		++q;
		t[q].a=input()-1,t[q].b=input()-1,t[q].c=input()-1,t[q].d=input()-1;
		t[q].num=i;
		if (t[q].a>t[q].c || t[q].b>t[q].d)
			q--;
	}
	dfs(0,n-1,1,q);
	for (int i=1;i<=q;++i)
		if (ans[i])
			printf("Safe\n");
		else
			printf("Dangerous\n");
	return 0;
}
void dfs(int l,int r,int x,int y){//[l,r]表示行的區間,[x,y]表示詢問的區間
	if (l>r || x>y)
		return;
	int mid=l+r>>1;
	cnt=0;
	if (mat[mid][m-1]=='0'){
		bs[++cnt].reset();
		bs[cnt].set(m-1);
		f[mid][m-1]=bs+cnt;
	}
	else
		f[mid][m-1]=bs;
	for (int i=m-2;i>=0;--i)
		if (mat[mid][i]=='0'){
			++cnt;
			bs[cnt].reset();
			bs[cnt].set(i);
			if (mat[mid][i+1]=='0')
				bs[cnt].getor(f[mid][i+1]);
			f[mid][i]=bs+cnt;
		}
		else
			f[mid][i]=bs;
	for (int i=mid-1;i>=l;--i)
		if (mat[i][m-1]=='0' && mat[i+1][m-1]=='0')
			f[i][m-1]=f[i+1][m-1];
		else
			for (;i>=l;--i)
				f[i][m-1]=bs;//在轉移最後一列的時候,我們發現,如果當中有一個斷了,後面的就全斷了
	for (int i=mid-1;i>=l;--i)
		for (int j=m-2;j>=0;--j)
			if (mat[i][j]=='0')
				if (mat[i][j+1]=='0'){
					if (mat[i+1][j]=='0'){
						bs[++cnt].get(f[i][j+1],f[i+1][j]);
						f[i][j]=bs+cnt;
					}
					else
						f[i][j]=f[i][j+1];
				}
				else{
					if (mat[i+1][j]=='0')
						f[i][j]=f[i+1][j];
					else
						f[i][j]=bs;
				}
	if (mat[mid][0]=='0'){
		bs