zoukankan      html  css  js  c++  java
  • JZOJ100048 【NOIP2017提高A组模拟7.14】紧急撤离

    题目

    在这里插入图片描述

    题目大意

    给你一个01矩阵,每次询问从一个点是否可以走到另一个点。
    每次走只能往右或者往下。


    思考历程

    这题啊,我想的时候真的是脑洞大开……
    首先,我一眼看下去,既然要询问是否联通,那么能不能求出它们的最短路,看看是不是它们的曼哈顿距离?
    看到数据范围之后这个想法彻底凉凉……
    然后就开始考虑一些正经的方法……
    首先,考虑如何扫描线……类似扫描线的,扫一扫,维护一下,说不定就可以了呢?
    然后,我发现无论如何,我都难以逃脱O(n2m2)O(n^2m^2),就算是使用bitset也不行。
    这样不行啊,我就考虑分治?
    如何分治?
    每次在中间的这一条线上开始,向两边进行转移。转移什么?转移每个点到这条线上连通的状态……
    bitset可以优化一下。
    但是又感觉,这个方法好像过不去,所以,我又继续向其他的做法。
    然后就想到了分块!
    如何分块呢?设块的行数为KK,那么我们在每个块的边界那里,搞一搞类似上面的转移。
    然后我们在所有的边界之间,求出它们的连通性。
    推了一波复杂度,好想很优秀!
    好开心好开心……
    然后打了几行,突然发现:时间复杂度好像算错了!
    非常不爽,又算了一遍。发现还是算错了,再算一遍……
    什么,这么慢,连分治都不如?
    又看看时间,似乎不多了……
    我绝望地打了个暴力,用bitset随便优化了一下……


    正解

    其实正解在比赛时已经想到了。
    只不过觉得过不了……
    这题的正解就是分治,和上面说的一模一样!
    非常不爽……
    这次说详细一些:
    按行或列分治(其实应该按列分治,具体原因……),下面一行为准。
    我们将矩阵分成上下两个部分。
    对于上面,我们设fi,jf_{i,j}表示点(i,j)(i,j)到中间的这一行上每个点的状态。
    对于下面,我们设gi,jg_{i,j}表示中间这一行上的每个点到(i,j)(i,j)的状态。
    其实两个是相反的,具体怎么转移显然。
    那么对于询问的两个点,如果它们之间的路径上会经过这一行,那就枚举经过行上的哪一个点,计算一下是否联通就好了。至于没有经过的,直接递归分治下去。

    然后分析一下时间复杂度。
    首先我们知道,很显然的,分治只有lgnlg n层。
    对于每一层,我们需要处理nm2nm^2次,因为我们考虑同一列上的点,它们转移所耗费的时间为nmnm,即为整个平面。由于每一行有mm个东西,所以就是nm2nm^2次。
    综上,转移的总时间是O(nm2lgn)O(nm^2lg n)
    然后就是处理询问的时间。
    我们首先将询问全部列在一起,然后在处理的时候,将左边的区间放左边,将右边的区间放右边,穿过中间行的区间直接处理。时间复杂度可以这么理解:对于每一个询问,它相当于在这棵分治所形成的的二叉树上面往下走,那么每个的时间为O(lgn)O(lg n)。然后我们一共有qq个询问,所以时间复杂度为O(qlgn)O(qlg n)。还有每次处理一个询问都需要O(qm)O(qm)的时间
    为什么时间复杂度好像和题解不一样,难道是我分析错了?还是lgnlg n太小以至于题解不屑于注意?
    综上所述,时间复杂度是O(nm2lgn+q(m+lgn))O(nm^2lg 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
    ");
    		else
    			printf("Dangerous
    ");
    	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[++cnt].reset();
    		bs[cnt].set(0);
    		g[mid][0]=bs+cnt;
    	}
    	else
    		g[mid][0]=bs;
    	for (int i=1;i<=m-1;++i)
    		if (mat[mid][i]=='0'){
    			bs[++cnt].reset();
    			bs[cnt].set(i);
    			if (mat[mid][i-1]=='0')
    				bs[cnt].getor(g[mid][i-1]);
    			g[mid][i]=bs+cnt;
    		}
    		else
    			g[mid][i]=bs;
    	for (int i=mid+1;i<=r;++i)
    		if (mat[i-1][0]=='0' && mat[i-1][0]=='0')
    			g[i][0]=g[i-1][0];
    		else
    			for (;i<=r;++i)	
    				g[i][0]=bs;
    	for (int i=mid+1;i<=r;++i)
    		for (int j=1;j<=m-1;++j)
    			if (mat[i][j]=='0')
    				if (mat[i][j-1]=='0'){
    					if (mat[i-1][j]=='0'){
    						bs[++cnt].get(g[i][j-1],g[i-1][j]);
    						g[i][j]=bs+cnt;
    					}
    					else
    						g[i][j]=g[i][j-1];
    				}
    				else{
    					if (mat[i-1][j]=='0')
    						g[i][j]=g[i-1][j];
    					else
    						g[i][j]=bs;
    				}
    	swap(t,tmp);//tmp表示原数组,t表示新数组,在这里交换只会交换指针
    	int i=x-1,j=y+1;
    	for (int k=x;k<=y;++k)
    		if (tmp[k].c<mid)//将在中间线上边和下边的区间分开,分别放在一块
    			t[++i]=tmp[k];
    		else if (tmp[k].a>mid)
    			t[--j]=tmp[k];
    		else
    			for (int l=tmp[k].b;l<=tmp[k].d;++l)
    				if (mat[mid][l]=='0' && (*f[tmp[k].a][tmp[k].b])[l] && (*g[tmp[k].c][tmp[k].d])[l]){
    					ans[tmp[k].num]=1;
    					break;
    				}
    	dfs(l,mid-1,x,i);
    	dfs(mid+1,r,j,y);
    	swap(t,tmp);//仔细想想可以发现,由下一层之间不会互相造成影响,所以这样做不会影响它的正确性。
    }
    

    简略地说一说我的各种优化

    结合代码更加清晰易懂(清晰易懂个鬼啊!)
    1、我们发现在DP转移的时候,经常会出现只转移一个或者是不转移的现象。那我们想一想,如果再用一个bitset将其存下,是不是一种浪费呢?因此,我们可以让几个转态共享一个结果。我们在一开始开好一个bitset内存池,在使用的时候,如果要开新值,那就在内存池里面分配一个,计算之后将当前的指针指向它。如果不用开新值,那就将当前的指针指向这个旧值的位置。
    2、询问连成块。其实这个是本来就要做的,算不得优化。如果询问连成块,那就比较好处理,在枚举过程中就不会枚举到当前区间外的询问。我们可以用另一个数组,然后扫一扫区间内的所有询问,将其放在那个数组中,分成两边。然后在递归下去的时候,将两个数组交换(其实这样的正确性是很好保证的。因为处理完大区间后,小区间不会对大区间产生影响,并且小区间也不会互相产生影响,所以是正确的。还有,我发现了一直以来,我都犯了个错误:实际上,C++中的swap是交换数组里面的值,而不是交换指针。所以,直接用指针来指向数组就好了。)
    3、特殊情况分开考虑,少一个if语句就少一个if语句,宁可码量大一些。
    4、手打bitset。自己打的总感觉常数会小一些,并且可以直接压6464位!还有实际上bitset很好打。
    5、读入优化:不解释。
    6、各种剪枝判断……
    7、开O2(这个优化就可以顶的过所有的优化)
    说实在的,我的卡常技术已经大不如前了。


    总结

    首先,要信任你的时间复杂度……
    如果时间复杂度是在10810^8次方以内的,恭喜你,一般都可以过。
    如果差不多的就要卡卡常数……
    最后,保持信仰,在正式比赛的时候出题人不会出这样的数据……

  • 相关阅读:
    CentOS7使用集群同步脚本对文件同步分发
    CentOS7安装jdk1.8
    CentOS7+CDH5.12.1集群搭建
    nginx通配符
    Nginx反向代理及配置
    一些好玩有用的书签
    linux操作小技巧锦集
    系统压测结果对比:tomcat/thinkphp/swoole/php-fpm/apache
    python修改linux日志(logtamper.py)
    【原创】给定随机数的取值范围(最小值、最大值),且要求多次取得的随机数最后的结果有一个固定的平均值
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145251.html
Copyright © 2011-2022 走看看