「CEOI2015 Day2」核能国度 题解

核能国度 题解

题意

\(~~~~\) 给出 \(W \times H\) 的矩形,\(N\) 个修改,每个修改有位置及参数 \(a,b\) ,表示对其周边距离它切比雪夫距离为 \(d\) 的格子的权值增加 \(\max(0,a-b\times d)\) 。最后 \(Q\) 组询问,每次求一个子矩阵的和。

题解

\(~~~~\) 我不会告诉你我做这道题做了半个月并且实现还借助了题解。(虽然有一周在期末考试。

Solution 1 暴力

\(~~~~\) 每次暴力修改其影响到的格子的权值,每次查询暴力求子矩阵的和,这个不多说。

\(~~~~\) 期望得分:???

Solution 2 二维前缀和

\(~~~~\) 暴力修改权值后做一遍二维前缀和,每次查询 \(\mathcal{O}(1)\) 回答。

\(~~~~\) 期望得分:\(\texttt{25pts}\)

Solution 3 一维差分

\(~~~~\) 观察到有 \(\texttt{40pts}\) 给在 \(H=1\) ,那么此时整个矩形可以被看作是一排数。设某个横坐标为 \(x\) 的修改能影响到的最远的距离 \(\dfrac{a}{b}=d\) ,那么就相当于给 \([x-d,x]\) 加上一个首项为 \(a \bmod b\) ,公差为 \(b\) ;给 \([x+1,x+d]\) 加上一个首项为 \(a-b\) ,公差为 \(-b\) 的等差数列。

\(~~~~\) 此时套路地维护这个数列的差分数列,那么需要支持区间加法,单点修改,且询问在所有修改之后。因此用差分维护差分数列,还原后再用一维前缀和回答即可。

\(~~~~\) 期望得分:结合 Solution 2 可得 \(\texttt{50pts}\)(部分子任务重合)。

Solution 3.5 半个正解的二维差分

\(~~~~\) 你已经想到用差分维护一维差分数列了,那么我们来随便举个例子看二维的情况:

\(~~~~\) 这是一个 \(a=7,b=2\) 的例子,(虽然画错了),但不难看出在差分后的规律:

\(~~~~\bullet\) 左上和右下角为 \(a \bmod b\),右上和左下角为 \(- a \bmod b\)
\(~~~~\bullet\) 除开四角,主对角线全为 \(b\) ,副对角线全为 \(-b\)

\(~~~~\) 当然直接根据差分的式子也能得到这个规律,这里为了直观就用找规律了。

\(~~~~\) 那么我们暴力 (指 O(1)) 修改四角,然后差分维护对角线即可。

\(~~~~\) 时间复杂度:\(\mathcal{O}(N+WH+Q)\)

\(~~~~\) 期望得分:结合 Solution 2 和 Solution 3 可得 \(\texttt{75pts}\)

\(~~~~\) 这里贴一个我写了三次才写出来的仅能得新增的 \(\texttt{25pts}\) 的代码:

查看代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
const ll MAXN=2500005;
ll n,m,k,q;
struct Array{ll mp[50000000];inline ll* operator [] (const ll& x){return mp+(x+(n+1)+1)*(3*m+1);}}Pre,Dia1,Dia2;//用指针开数组
int main() {
	scanf("%lld %lld %lld",&n,&m,&k);
	while(k--)
	{
		ll x,y;ll a,b;
		scanf("%lld %lld %lld %lld",&x,&y,&a,&b);
		ll Up=a/b;
		if(x-Up>0) Pre[x-Up][y-Up]+=a%b;Pre[x+Up+1][y+Up+1]+=a%b;
		Pre[x+Up+1][y-Up]-=a%b;Pre[x-Up][y+Up+1]-=a%b;
		
		Dia1[x-Up+1][y-Up+1]+=b;Dia1[x+Up+1][y+Up+1]-=b;
		Dia2[x+Up][y-Up+1]-=b;Dia2[x-Up][y+Up+1]+=b;
	}
	for(ll j=1;j<=m;j++)
	{
		for(ll i=1;i<=n;i++)
		{
			Dia1[i][j]+=Dia1[i-1][j-1],Dia2[i][j]+=Dia2[i+1][j-1];
			Pre[i][j]+=Dia1[i][j]+Dia2[i][j];
			Pre[i][j]+=Pre[i-1][j]+Pre[i][j-1]-Pre[i-1][j-1];
		}
	}
	for(ll j=1;j<=m;j++) for(ll i=1;i<=n;i++) Pre[i][j]+=Pre[i-1][j]+Pre[i][j-1]-Pre[i-1][j-1];
	ll Q;scanf("%lld",&Q);
	while(Q--)
	{
		ll X1,X2,Y1,Y2;
		scanf("%lld %lld %lld %lld",&X1,&Y1,&X2,&Y2);
		ll Siz=(Y2-Y1+1)*(X2-X1+1);
		printf("%lld\n",(ll)((Pre[X2][Y2]-Pre[X1-1][Y2]-Pre[X2][Y1-1]+Pre[X1-1][Y1-1])*1.0/Siz+0.5));
	}
	return 0;
}

Solution 4 加了亿些细节的二维差分

\(~~~~\) 事实上 ,上面算法的时间复杂度是对的,但它并不能得全分。如果你仔细观察题目,你会看到这样一句话:

\(~~~~\) 如果核电站位于核能国的边境或是在离边境稍近的位置,那么爆炸可能也会影响到核能国之外的方格。影响到核能国外方格的爆炸被称作界限

\(~~~~\) 而上面 Solution3.5 新增的 \(\texttt{25pts}\) 来自没有界限的子任务。仔细思考一下,我们会发现如果爆炸影响到的格子在当前矩形的左、左上或上方时,差分标记会影响内部的值,但如果出界了我们统计不到那部分。

\(~~~~\) 而且本题我们无法通过扩大若干倍矩形来强行统计那些部分,考虑一个极端情况:\(W=H=1\) ,然后在那个格子上有一个 \(a=2^{63},b=1\) 的修改。

\(~~~~\) 以下涉及大量代码实现,其中 \(n\)\(m\) 是题面中 \(W\) \(H\)

\(~~~~\) 那么我们分别对每一种标记来考虑怎么处理出界的问题:

\(~~~~\) 对于四角的修改,我们强行移动它到对应的第一个生效的位置即可,换句话说就是把小于 \(1\) 的坐标移动到 \(1\)

查看代码
void Tag(ll X1,ll Y1,ll X2,ll Y2,ll V)
{
	S[X1][Y1]+=V;S[X2+1][Y1]-=V;
	S[X1][Y2+1]-=V;S[X2+1][Y2+1]+=V;
}
Tag(max(x-Up,1ll),max(y-Up,1ll),min(x+Up,n),min(y+Up,m),a%b-b);//调用,注意后面即使对答案不影响也不能不取min,否则会RE。
// 最后对于四角-b,则整个对角线都+b即可

\(~~~~\) 对于主对角线的修改,我们将其超出部分的全部移动到第一行或第一列对应的位置,这个需要分类讨论几种情况。

\(~~~~\) 先写一个给第一行/列打标记的函数:

查看代码
ll A[MAXN],B[MAXN];//记超出部分给 第一行 和 第一列 打的标记 
void Sign(ll *Arr,ll l,ll r,ll v) {Arr[l]+=v,Arr[r+1]-=v;} //一阶差分,对 [l,r] +v ,记在第一行或第一列 

\(~~~~\) 然后对超出的左上角部分进行处理:

查看代码
void TagLU(ll X1,ll Y1,ll X2,ll Y2,ll V)//[X1,Y1]:起始 [X2,Y2]:最后一个超出的格子
{
	if(X1>X2)return;
	if(X2<=0&&Y2<=0) Sign(A,1,1,(X2-X1+1)*V); // Area 1:全部在左上 
	else if(X2<=0)//Area 2:左
	{
		if(Y1<=0) Sign(B,1,Y2,V),Sign(B,1,1,(1-Y1)*V);//有一部分在左上
		else Sign(B,Y1,Y2,V);//全在左
	}
	else if(Y2<=0)//Area 3:上
	{
		if(X1<=0) Sign(A,1,X2,V),Sign(A,1,1,(1-X1)*V);//有一部分在左上
		else Sign(A,X1,X2,V);//全在上
	}
}
TagLU(X-Up,Y-Up,X-min(min(X-1,Y-1),Up)-1,Y-min(min(X-1,Y-1),Up)-1,b);

\(~~~~\) 以及打起始和结束的标记:

查看代码
Dia1[X-min(min(X-1,Y-1),Up)][Y-min(min(X-1,Y-1),Up)]+=b;
Dia1[X+1+min(Up,min(n-X-1,m-Y))+1][Y+1+min(Up,min(n-X,m-Y-1))+1]-=b;

\(~~~~\) 然后是副对角线,大体同上,但注意左右都可能有超出。

查看代码
void TagRU(ll Y1,ll Y2,ll V){if(Y1>=Y2&&Y2<=m) Sign(B,Y2,min(Y1,m),-V);}
void TagLD(ll X1,ll X2,ll V){if(X1<=X2&&X1<=n) Sign(A,X1,min(X2,n),-V);}
void Tag(ll X,ll Y,ll a,ll b)
{
	ll Up=a/b;
	TagRU(Y+1+Up,Y+1+min(Up,min(X-1,m-Y-1))+1,b);
	TagLD(X+1+min(Up,min(n-X-1,Y-1))+1,X+1+Up,b);
	Dia2[X-min(Up,min(X-1,m-Y-1))][Y+1+min(Up,min(X-1,m-Y-1))]-=b;
	Dia2[X+1+min(Up,min(n-X-1,Y-1))+1][Y-min(Up,min(n-X-1,Y-1))-1]+=b;
}

\(~~~~\) 最后把对角线的差分标记还原,把第一行和第一列归到一起

查看代码
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) Dia1[i][j]+=Dia1[i-1][j-1],Dia2[i][j]+=Dia2[i-1][j+1];
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) Dia1[i][j]+=Dia2[i][j];
for(int i=1;i<=n;i++) Dia1[i][1]+=(A[i]+=A[i-1]);
for(int i=1;i<=m;i++) Dia1[1][i]+=(B[i]+=B[i-1]);

\(~~~~\) 然后就和上面没有任何区别了。轻松而又愉快。

\(~~~~\) 时间复杂度:同 Solution3.5

\(~~~~\) 期望得分:\(\texttt{100pts}\)

\(~~~~\) 完整代码就不贴了,整体很丑。

posted @ 2021-02-03 22:02  Azazеl  阅读(133)  评论(0编辑  收藏  举报