【洛谷4800】[CEOI2015 Day2] 核能国度(差分细节题)

点此看题面

  • 给定一个(W imes H)的网格图,其中有(n)个点上的核电站核泄漏了。
  • 每个核电站有两个参数(a_i,b_i),表示距离它契比雪夫距离为(d)的点将会受到(max{a_i-b_i imes d,0})单位的核辐射。
  • (q)次询问,每次求一个子矩阵中所有点受到的平均辐射量。
  • (W imes Hle2.5 imes10^6,n,qle10^5)

影响范围不越界时的直接差分

考虑对于一个核电站,它的贡献是一圈一圈向外逐渐减少的。

其实可以看作先给以它为中心,边长为(2 imeslfloorfrac ba floor+1)的正方形区域都造成((a\%b)-b)单位的核辐射,然后给所有以它为中心,边长为(2 imes i+1(iin[0,lfloorfrac ab floor]))的正方形各自都造成(b)点核辐射。

其中第一部分是会对范围内的每个格子造成同样的伤害,直接普通二维差分一下就好了。

但第二部分就复杂许多,因此我们考虑一种对角线差分的方式

绿色的正方形表示影响区域,黑点为中心。则我们的差分方式是给所有红点(包括黑点)打上差分标记(b),给所有蓝点打上差分标记(-b)

此时只要做一次二维前缀和就能实现一次修改了。

但我们显然不可能去枚举对角线上的每一个点给它打标记。

实际上这里可以使用二阶差分优化,也就是给差分标记再差分,那么又变成单点的修改了。

影响范围越界

上面的东西都是在影响范围不越界时考虑的,而越界时就会有些麻烦了。

实际上,由于我们做的是二维前缀和,如果落在网格图右边或下面显然不可能产生任何影响。

否则,对于第一区域的标记,可以直接移到((1,1)),而第二、第三区域的标记分别可以移到对应列的第一行和对应行的第一列。

那么只要给第一行或第一列打上二阶差分标记,做完一遍前缀和之后再归到总差分标记中就好了。

代码:(O(HW+n+q))

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define SZ 10000000
#define int long long
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
using namespace std;
int w,h,n,A[SZ+5],B[SZ+5];
struct Array {int a[SZ+5];I int* operator [] (CI x) {return a+x*(h+2);}}S,P,Q;//用指针开数组
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define pc(c) (C==E&&(clear(),0),*C++=c)
		#define D isdigit(c=tc())
		int T;char c,*A,*B,*C,*E,FI[FS],FO[FS],S[FS];
	public:
		I FastIO() {A=B=FI,C=FO,E=FO+FS;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
		Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
		Tp I void writeln(Ty x) {W(S[++T]=x%10+48,x/=10);W(T) pc(S[T--]);pc('
');}
		I void clear() {fwrite(FO,1,C-FO,stdout),C=FO;}
}F;
#define DS(x1,y1,x2,y2,v) (S[x1][y1]+=v,S[x1][y2+1]-=v,S[x2+1][y1]-=v,S[x2+1][y2+1]+=v)//二维差分
I void TagS(CI x,CI y,CI a,CI b)//给影响范围打上一个相同的标记
{
	DS(max(x-a/b,1),max(y-a/b,1),min(x+a/b,w),min(y+a/b,h),a%b-b);
}
#define DT(T,l,r,v) (T[l]+=v,T[(r)+1]-=v)//一维差分
I void WorkP(CI x1,CI y1,CI x2,CI y2,CI b)
{
	if(x1>x2) return;if(x2<1&&y2<1) return (void)DT(A,1,1,(x2-x1+1)*b);//完全落在第一部分
	if(x2<1) y1>=1?DT(B,y1,y2,b):(DT(B,1,y2,b),DT(B,1,1,(1-y1)*b));//落在第二部分,判断是否经过第一部分
	if(y2<1) x1>=1?DT(A,x1,x2,b):(DT(A,1,x2,b),DT(A,1,1,(1-x1)*b));//落在第三部分,判断是否经过第一部分
}
I void WorkQ1(CI y1,CI y2,CI b) {y1>=y2&&y2<=h&&DT(B,y2,min(y1,h),-b);}//落在第二部分
I void WorkQ2(CI x1,CI x2,CI b) {x1<=x2&&x1<=w&&DT(A,x1,min(x2,w),-b);}//落在第三部分
I void TagPQ(CI x,CI y,CI a,CI b)//给对角线打标记
{
	WorkP(x-a/b,y-a/b,x-min(a/b,min(x-1,y-1))-1,y-min(a/b,min(x-1,y-1))-1,b),//主对角线超出范围的左上部分
	WorkQ1(y+1+a/b,y+1+min(a/b,min(x-1,h-y-1))+1,b),//副对角线超出范围的右上部分
	WorkQ2(x+1+min(a/b,min(w-x-1,y-1))+1,x+1+a/b,b),//副对角线超出范围的左下部分
	P[x-min(a/b,min(x-1,y-1))][y-min(a/b,min(x-1,y-1))]+=b,//主对角线二阶差分
	P[x+1+min(a/b,min(w-x-1,h-y))+1][y+1+min(a/b,min(w-x,h-y-1))+1]-=b,
	Q[x-min(a/b,min(x-1,h-y-1))][y+1+min(a/b,min(x-1,h-y-1))]-=b,//副对角线二阶差分
	Q[x+1+min(a/b,min(w-x-1,y-1))+1][y-min(a/b,min(w-x-1,y-1))-1]+=b;
}
signed main()
{
	RI i,j,x,y,a,b;for(F.read(w,h,n),i=1;i<=n;++i)
		F.read(x,y,a,b),TagS(x,y,a,b),TagPQ(x,y,a,b);
	#define REP for(i=1;i<=w;++i) for(j=1;j<=h;++j)//为使代码简洁,#define循环
	REP P[i][j]+=P[i-1][j-1],Q[i][j]+=Q[i-1][j+1];REP P[i][j]+=Q[i][j];//一遍前缀和求出差分标记并归到P
	for(i=1;i<=w;++i) P[i][1]+=(A[i]+=A[i-1]);//第一列的差分标记
	for(i=1;i<=h;++i) P[1][i]+=(B[i]+=B[i-1]);//第一行的差分标记
	REP P[i][j]+=P[i-1][j]+P[i][j-1]-P[i-1][j-1];//差分标记做前缀和求出实际值
	REP S[i][j]+=S[i-1][j]+S[i][j-1]-S[i-1][j-1];//原本打上的相同标记做前缀和求出实际值
	REP S[i][j]+=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+P[i][j];//二维前缀和方便询问
	RI Qt,t;F.read(Qt);W(Qt--) F.read(x,y,a,b),
		(t=S[a][b]-S[x-1][b]-S[a][y-1]+S[x-1][y-1])<0&&(t+=1ull<<63),//一个小技巧
		F.writeln((int)(1.0L*t/((a-x+1)*(b-y+1))+0.5));//计算答案并输出
	return F.clear(),0;
}
原文地址:https://www.cnblogs.com/chenxiaoran666/p/Luogu4800.html