5 7 级 错 位 打 ♂ 击 赛

5 7 级 错 位 打 击 赛

本篇博客并不会讲任何算法,所以极水

博主写这篇博客完全是出于在机房颓废没有事情做,顺便整理一下模拟考试的题目,所以不会像之前那样完善

T1

传送门:https://www.luogu.com.cn/problem/U121235?contestId=31441

显然,这是一个数据结构题(线段树),但是由于考试的时候没有推出标记下传的方法来,所以写了一个暴力,拿了40分的部分分

因为本人写的代码实在太糟糕了(其实是懒得写,反正有学长的详细思路解释,我就直接\(copy\)啦),所以以下是学长的T1题解

线段树板子题没人 \(AC\) 嘛?

前言

出题人本来打算给你们 \(40pts\) 的暴力分,结果没想到数据出水了,你们暴力竟然能拿 \(70pts\),给了你们一个 \(T1\) 很好做的样子。

本着信心赛的原则,我还是很希望你们能多拿一点分数,给教练一个肯定的回答,让大家的分数好看点。

还是很希望你们能认真地听完 \(30pts\) 的正解分。

当然现在我加强了数据,暴力只能拿 \(40pts\) 了吧。

\(10pts\)

对于 \(10\)% 的数据,\(x=0\)

操作 \(1\) 再怎么花里胡哨都是没用的,我们只需要考虑操作 \(2\) 即可。

区间求和问题,可以直接用前缀和来做,时间复杂度 \(Θ(n)\)

期望得分 \(:10pts\)

\(20pts\)

对于另外 \(10\)% 的数据,\(1<=l=r<=n\)

只是多了一个单点修改的操作,只需注意我们在修改的时候是 \(-x\),可以用线段树或树状数组来做,时间复杂度 \(Θ(nlog_n)\)

期望得分 \(:20pts\)

\(40pts\)

对于另外 \(20\)% 的数据,\(1<=n,m<=5000\)\(-100<=a_i,x<=100\)

考虑到 \(n\)\(m\) 的范围很小,我们直接用最朴素的算法 "暴力" 直接做即可,时间复杂度 \(Θ(nm)\)

\(100pts\)

对于 \(100\)%的数据,保证 \(1<=n,m<=3*10^5\)\(-1000<=a_i,x<=1000\)\(1<=l,r<=n\)

看到这个数据范围,正解肯定是 \(Θ(nlog_n)\) 的数据结构无疑了 。

由于出题人没有什么好的做法,那么这道题我们还是用线段树做吧。

思考一下这道题和你们做的线段树板子有什么不同,很显然它的操作 \(1\) 变得更加毒瘤且恶心。

但是换汤不换药,线段树板子的套路在这道题上还是可以应用的,那就是:懒标记

回顾一下区间加、区间求和的懒标记:

if(x<=l&&r<=y)
{
	lazy[node]+=v;
	sum[node]+=(r-l+1)*v;
	return ;
}

比着葫芦画瓢,我们做这道题的时候也设置一个懒标记:

\(lazy1[node]=k\) 表示线段树中 \(node\) 节点所代表的区间进行了一次操作 \(1\),要时刻注意操作 \(1\) 是以 \(-\) 开始的。

然后我们思考一下怎么 \(Θ(1)\) 地对 \(sum[node]\) 进行维护:

找规律:我们可以将第 \(k\) 个和第 \(k+1\) 个看成一组,它们的贡献之和为 \(x\) ,那么所有组的贡献总和为:\(x*\) 组数 :

但是可能还会存在一个落单的,别忘了计算这个落单的贡献。

对于 \(lazy1[node]\) 的维护,我们直接令 \(lazy1[node]+=k\) 即可 。

这样就可以了嘛?真 · 线段树板子?上述做法有什么问题嘛?

\(Problem1:\) 正负号问题 。

一个很重要的问题:我们上面的操作都是基于第一个数是 \(-\) 才行,也就是说,我们的操作形式要和题目中给出的操作 \(1\) 的形式一直才对 。

题目中的操作 \(1\) 不是以 \(-\) 的形式开始的嘛?为什么当前区间第一个数可能会是 \(+\) 的形式?

如上图所示:如果你要对 \([1,5]\) 进行操作 \(1\) ,当你递归到你的右儿子 \([4,5]\) 的时候发现是以 \(+\) 开头的,但是由于左儿子的最左端就是它父亲的最左端,所以左儿子开头的正负性和父亲开头的正负性是相等的,我们比较好判断,我们重点要判断右儿子的开头的正负性 。

想一想哈,如果左儿子的区间长度为 \(2k\),那么右儿子开头的正负性和父亲的相同;如果左儿子的区间长度为 \(2k+1\),那么右儿子开头的正负性和父亲的相反 。

\(Problem2:\) 标记下传问题

注意一下我们 \(lazy1[node]=k\) 的定义:表示线段树中 \(node\) 节点所代表的区间进行了一次操作 \(1\)

注意到操作 \(1\) 是有两个性质的:

①. 正负号交替;②. 每一项的绝对值之差为首项的绝对值。

注意到上图右儿子中,操作 \(1\) 的首项为 \(+4x\) ,而每一项的绝对值之差为 \(x\),不符合操作 \(1\) 的定义了,所以我们不能直接维护懒标记。

提取公因式:我们将 \(+4x\) 看作是 \(+3x+x\),将 \(-5x\) 看作是 \(-3x-2x\),注意到右边的 \(+x\)\(-2x\) 了没,这不就是操作 \(1\) 嘛?对于左边的 \(+3x\)\(-3x\) 呢,我们可以看出一种新的操作:

\(3.l,r,y\) 表示将 \([l,r]\) 的第 \(l\) 个数减 \(y\),第 \(l+1\) 个数加 \(y\),第 \(l+2\) 个数减 \(y\) ......

即:令 \(a_l=a_l-y\)\(a_{l+1}=a_{l+1}+y\)\(a_{l+2}=a_{l+2}-y\) ...... \(a_r=a_r+(-1)^{r-l+1}y\)

所以对于上面的右儿子,我们可以看作是先进行了一次 \(x=-x\) 的操作 \(1\) ,再加上一次 \(y=-y\) 的操作 \(3\)

对于操作 \(3\),我们同样开一个懒标记 \(lazy2[node]=k\) 来进行维护,有 \(1\) 必有 \(2\)\(qwq\)

\(insert\)

void insert(int node,int l,int r,int x,int y,int k)
{
	if(x<=l&&r<=y)       //[l,r]被完全覆盖 
	{
		int len=r-l+1;   //当前区间的长度 
		int d=l-x;       //x~l-1之间有多少个数 
		if(d&1)          //如果l前面有有奇数个数,说明第l个数在[x,y]内是第偶数个数,那么应该是从加号开始 
		{
			lazy1[node]-=k;  //由于我们lazy1的设定是从减号开始的,所以这里应该是-= 
			lazy2[node]-=k*d;
			   //将操作1进行拆解,所提取出来的公因数的绝对值为k*d,同样我们操作3的设定也是从减号开始,所以这里还是-= 
			
			sum[node]-=(len/2)*k;  //对sum[node]进行维护,将它们两两看成一组,一共有len/2组,每一组的贡献为-k 
			if(len&1) sum[node]+=k*(r-x+1); 
			   //如果区间长度为奇数,说明两两一组有余,由于我们已经判断出该区间是从加号开始的,所以这个余出的数一定是加 
		}
		else             //从减开始,与上面的同理,只是符号都改成相反的而已 
		{
			lazy1[node]+=k;
			lazy2[node]+=k*d;
			sum[node]+=(len/2)*k;
			if(len&1) sum[node]-=k*(r-x+1);
		}
		return ;
	}
	pushdown(node,l,r);  //记得标记下传   
	int mid=(l+r)>>1;
	if(x<=mid) insert(node<<1,l,mid,x,y,k);
	if(y>mid) insert(node<<1|1,mid+1,r,x,y,k);
	update(node);        //维护父亲节点的sum 
}

\(pushdown\)

如果你已经理解了 \(insert\) 的思路,那么 \(pushdown\) 的思路也就不难了。

void pushdown(int node,int l,int r)
{
	int mid=(l+r)>>1;
	int len=r-l+1;
	int len1=mid-l+1;
	int len2=r-mid;
	if(lazy1[node])                           //时刻牢记lazy1是从减开始的 
	{
		sum[node<<1]+=(len1/2)*lazy1[node];   //两两分组算贡献 
		if(len1&1) sum[node<<1]-=len1*lazy1[node];  //两两分组有余,那么余的这个数肯定是减的 
		lazy1[node<<1]+=lazy1[node];          //维护左儿子的lazy1 
		if(len1&1)                            //如果左儿子的区间长度为奇数,说明右儿子的左端点是第偶数个数,则是从加开始 
		{
			lazy1[node<<1|1]-=lazy1[node];    //由于是从加开始,所以这里是减 
			lazy2[node<<1|1]-=lazy1[node]*len1; 
			   //只有右儿子不满足操作1的性质②,因此我们要拆分操作,所以我们只维护右儿子的lazy2 
			sum[node<<1|1]-=(len2/2)*lazy1[node]; //两两分组算贡献 
			if(len2&1) sum[node<<1|1]+=lazy1[node]*len; //两两分组有余,由于右儿子是从加开始的,所以余的这个数肯定是加的 
		} 
		else                                  //右儿子的左端点从减开始,除了符号和上面都一样 
		{
			lazy1[node<<1|1]+=lazy1[node];
			lazy2[node<<1|1]+=lazy1[node]*len1;
			sum[node<<1|1]+=(len2/2)*lazy1[node];
			if(len2&1) sum[node<<1|1]-=lazy1[node]*len;
		}
		lazy1[node]=0;                        //别忘了清空标记1 
	}
	if(lazy2[node])                           //下传标记2 
	{
		if(len1&1) sum[node<<1]-=lazy2[node]; //对于操作3,发现两两算贡献的时候可以抵消,所以如果有余肯定是减的 
		lazy2[node<<1]+=lazy2[node];          //维护左儿子的lazy2 
		if(len1&1)                            //右儿子的左端点从加开始    
		{
			lazy2[node<<1|1]-=lazy2[node];    //维护右儿子的lazy2,由于右儿子的左端点是从加开始的,所以这里是减 
			if(len2&1) sum[node<<1|1]+=lazy2[node];
		} 
		else                                  //右儿子的左端点从减开始,仍然只有符号不同 
		{
			lazy2[node<<1|1]+=lazy2[node];
			if(len2&1) sum[node<<1|1]-=lazy2[node];
		}
		lazy2[node]=0;                        //清空lazy2 
	}
}

完整版\(Code:\)

#include
#include
using namespace std;
int read()
{
	char ch=getchar();
	int a=0,x=1;
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') x=-x;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		a=(a<<1)+(a<<3)+(ch-'0');
		ch=getchar();
	}
	return a*x;
}
const int N=1e5+5;
int n,m;
int a[N],sum[N<<2],lazy1[N<<2],lazy2[N<<2];
void update(int node)
{
	sum[node]=sum[node<<1]+sum[node<<1|1];
}
void pushdown(int node,int l,int r)
{
	int mid=(l+r)>>1;
	int len=r-l+1;
	int len1=mid-l+1;
	int len2=r-mid;
	if(lazy1[node])                           //时刻牢记lazy1是从减开始的 
	{
		sum[node<<1]+=(len1/2)*lazy1[node];   //两两分组算贡献 
		if(len1&1) sum[node<<1]-=len1*lazy1[node];  //两两分组有余,那么余的这个数肯定是减的 
		lazy1[node<<1]+=lazy1[node];          //维护左儿子的lazy1 
		if(len1&1)                            //如果左儿子的区间长度为奇数,说明右儿子的左端点是第偶数个数,则是从加开始 
		{
			lazy1[node<<1|1]-=lazy1[node];    //由于是从加开始,所以这里是减 
			lazy2[node<<1|1]-=lazy1[node]*len1; 
			   //只有右儿子不满足操作1的性质②,因此我们要拆分操作,所以我们只维护右儿子的lazy2 
			sum[node<<1|1]-=(len2/2)*lazy1[node]; //两两分组算贡献 
			if(len2&1) sum[node<<1|1]+=lazy1[node]*len; //两两分组有余,由于右儿子是从加开始的,所以余的这个数肯定是加的 
		} 
		else                                  //右儿子的左端点从减开始,除了符号和上面都一样 
		{
			lazy1[node<<1|1]+=lazy1[node];
			lazy2[node<<1|1]+=lazy1[node]*len1;
			sum[node<<1|1]+=(len2/2)*lazy1[node];
			if(len2&1) sum[node<<1|1]-=lazy1[node]*len;
		}
		lazy1[node]=0;                        //别忘了清空标记1 
	}
	if(lazy2[node])                           //下传标记2 
	{
		if(len1&1) sum[node<<1]-=lazy2[node]; //对于操作3,发现两两算贡献的时候可以抵消,所以如果有余肯定是减的 
		lazy2[node<<1]+=lazy2[node];          //维护左儿子的lazy2 
		if(len1&1)                            //右儿子的左端点从加开始    
		{
			lazy2[node<<1|1]-=lazy2[node];    //维护右儿子的lazy2,由于右儿子的左端点是从加开始的,所以这里是减 
			if(len2&1) sum[node<<1|1]+=lazy2[node];
		} 
		else                                  //右儿子的左端点从减开始,仍然只有符号不同 
		{
			lazy2[node<<1|1]+=lazy2[node];
			if(len2&1) sum[node<<1|1]-=lazy2[node];
		}
		lazy2[node]=0;                        //清空lazy2 
	}
}
void build(int node,int l,int r)
{
	if(l==r) 
	{
		sum[node]=a[l];
		return ;
	}
	int mid=(l+r)>>1;
	build(node<<1,l,mid);
	build(node<<1|1,mid+1,r);
	update(node);
}
void insert(int node,int l,int r,int x,int y,int k)
{
	if(x<=l&&r<=y)       //[l,r]被完全覆盖 
	{
		int len=r-l+1;   //当前区间的长度 
		int d=l-x;       //x~l-1之间有多少个数 
		if(d&1)          //如果l前面有有奇数个数,说明第l个数在[x,y]内是第偶数个数,那么应该是从加号开始 
		{
			lazy1[node]-=k;  //由于我们lazy1的设定是从减号开始的,所以这里应该是-= 
			lazy2[node]-=k*d;
			   //将操作1进行拆解,所提取出来的公因数的绝对值为k*d,同样我们操作3的设定也是从减号开始,所以这里还是-= 
			sum[node]-=(len/2)*k;  //对sum[node]进行维护,将它们两两看成一组,一共有len/2组,每一组的贡献为-k 
			if(len&1) sum[node]+=k*(r-x+1); 
			   //如果区间长度为奇数,说明两两一组有余,由于我们已经判断出该区间是从加号开始的,所以这个余出的数一定是加 
		}
		else             //从减开始,与上面的同理,只是符号都改成相反的而已 
		{
			lazy1[node]+=k;
			lazy2[node]+=k*d;
			sum[node]+=(len/2)*k;
			if(len&1) sum[node]-=k*(r-x+1);
		}
		return ;
	}
	pushdown(node,l,r);  //记得标记下传   
	int mid=(l+r)>>1;
	if(x<=mid) insert(node<<1,l,mid,x,y,k);
	if(y>mid) insert(node<<1|1,mid+1,r,x,y,k);
	update(node);        //维护父亲节点的sum 
}
int query(int node,int l,int r,int x,int y)
{
	if(x<=l&&r<=y) return sum[node];
	pushdown(node,l,r);
	int mid=(l+r)>>1;
	int cnt=0;
	if(x<=mid) cnt+=query(node<<1,l,mid,x,y);
	if(y>mid) cnt+=query(node<<1|1,mid+1,r,x,y);
	return cnt; 
}
int main()
{
	//freopen("country.in","r",stdin);
	//freopen("country.out","w",stdout); 
	n=read();m=read();
	for(int i=1;i<=n;i++)
	    a[i]=read();
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int opt,l,r,x;
		opt=read();
		l=read();r=read();
		if(l>r) swap(l,r);
		if(opt==1) 
		{
			x=read();
			insert(1,1,n,l,r,x);
		}
		else printf("%d\n",query(1,1,n,l,r));
	}
	return 0;
} 

\(The\) \(last\)

线段树是一种很优秀的数据结构,希望你们能多加练习,体会它的魅力。

本着出于让你们练一练线段树的目的,我才出的这道题,没想到正解还真的不好做。出题人埋头苦思想了 \(2.5h\) 才想出正解,当然可能有更优的方法等待着你们来挖掘。

最后膜一下你们这群 \(Θ(n^2)\)\(1e4\) 数据的卡常带师。

​ ——暗ざ之殇

T2

讲道理,凭良心说话,T2要比T1简单亿

传送门:https://www.luogu.com.cn/problem/U121369?contestId=31441

\(30pts\)

**首先感谢学长不杀之恩,看到数据范围,了解到一个\(30pts\)的做法,因为\(n=1\),所以输出\(0\)即可

\(100pts\)

题目描述的已经很清楚了,把问题抽象化出来,就是求节点\(1\)的单源最短路,再求所有节点到节点\(1\)的单源最短路,累加答案即可

首先,我们看到,求所有到\(1\),求\(1\)到所有第一个想到的解法应该是\(floyd\)

这个算法可以在\(O(n^3)\)的时间内求出全源最短路,然后我们调用我们需要的数据,累加答案即可

但是显然这不是正解,因为:

对于\(100\)%的数据,有\(n<=1000\)

\(1000^3\)你玩呢?所以我又想出了如下方案:

1、跑\(n\)遍堆优化的\(dijkstra\),然鹅,复杂度\(O(n*(m+n)logn)\),并没有什么实质上的优化

2、\(n\)\(spfa\),最坏复杂度\(O(n^2*m)\),依然是我们无法接受的量级

暂时没有思路,所以我在小本子上把样例画了出来,发现了这样一个有趣的现象:

5 7 级 错 位 打 ♂ 击 赛_第1张图片

(原谅我是灵魂画手)

我们发现,如上图(假设边权都是\(1\)),我们跑一遍\(1\)->\(4\)的最短路,是\(1\)->\(3\)->\(4\),如果我们把边反着建,再跑一边最短路,就是\(1\)->\(4\),也就是正向图\(4\)->\(1\)的最短路

所以我直接乱搞一波反向建边+两遍\(dijkstra\)(反正是乐多赛制可以实时观测评测结果)

直接交上去发现,真就切掉了。。。

\(AC\) \(Code\)(后来我发现洛谷上还有一个几乎一模一样的题P1629,传送门:https://www.luogu.com.cn/problem/P1629

#include
#include
#include
#include
#include
#define N 1010
using namespace std;
typedef long long int ll;
priority_queue< pair >q;
ll mapp[N][N],dis[N],vis[N],cop[N],n,m,d,ans;
void upside_down(){
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
			swap(mapp[i][j],mapp[j][i]);//这里是反向建边(我比较懒直接用的邻接矩阵
}
void dijkstra(){
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[1]=0;
	q.push(make_pair(0,1));
	while(q.size()!=0){	
		int x=q.top().second;
		q.pop();
		vis[x]=1;
		for(int y=1;y<=n;y++){
			int z=mapp[x][y];
			if(dis[y]>dis[x]+z&&vis[y]!=1){
				dis[y]=dis[x]+z;
				q.push(make_pair(-dis[y],y)); 
			}
		}
	} 
}
int main(){
	scanf("%lld%lld%lld",&n,&m,&d);
	memset(mapp,0x7f,sizeof(mapp));
	for(ll i=0;i

T3

传送门:https://www.luogu.com.cn/problem/U121284?contestId=31441

又双叒叕是一个图论题!

\(50pts\)

仍然先感谢学长不杀之恩:对于\(50\)%的数据,\(n\)<=100,所以我如果没猜错的话,这\(50pts\)应该是给了暴搜吧……但是好像机房里没有人愿意拿这个部分分唉?

\(100pts\)

扫一眼题面,我们了解到,本题要求出从\(s\)->\(e\)的一条路径,使得这条路径上,最大的边权最小,并输出这个最大边权最小值

想了一通有没有这么一个算法来应对这种问题,,,但是显然没有

突然想到,本题的答案似乎具有单调性,可以使用二分法求解

具体怎么个单调性?

首先,我们二分一个中点\(Mid\),然后在图上跑一遍,如果遇到权值比\(Mid\)值要大的边,就自动忽视掉

然后,看看剩下的这些边,能不能支持我们从\(s\)->\(e\)

如果不走比\(Mid\)权值大的边,可以支持我们从\(s\)->\(e\),说明那个最大边权最小值比\(Mid\)小,反之,比\(Mid\)

然而还有一个问题,我们怎么判断这两个点在上述二分求解的情况中是否被联通呢?

一开始我想到了\(dfs\),但是经过我多年写暴搜\(TLE\)的惨痛教训,我真的不愿意写这个搜索了。。。

于是我又想起了一个玄学做法——跑\(dijkstra\),首先赋值\(dis\)数组为\(0x7f7f7f7f\),从\(s\)跑一遍\(dijkstra\),看看\(e\)的最短路有没有被更新,如果被更新了,说明\(s\),\(e\)联通,反之,\(s\)\(e\)不连通

然后我把这两个玄学的思路结合到了代码里:

\(Code\)

#include
#include
#include
#include
#include
using namespace std;
const int N=5005;
typedef long long int ll;
struct edge{
	int to,cost;
};
vectorv[N];
priority_queue< pair >q;
ll dis[N],vis[N],cop[N],n,m,s,e;
bool dijkstra(int a,int m){
	memset(dis,0x7f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[a]=0;
	q.push(make_pair(0,a));
	while(q.size()!=0){
		int x=q.top().second;
		q.pop();
		for(unsigned int i=0;im) continue;
			if(dis[y]>dis[x]+z){
				dis[y]=dis[x]+z;
				q.push(make_pair(-dis[y],y));
			}
		}
	}
	if(dis[e]>2147483646) return false;
	else return true;
}
int main(){
	scanf("%lld%lld",&n,&m);
	for(ll i=0,x,y,z;i>1;
		if(dijkstra(s,Mid)==true){
			r=Mid-1;
			ans=min(ans,Mid);
		}else l=Mid+1;
	}
	printf("%lld",ans);
	return 0;
}

结果\(AC\)了,比赛后学长发了一下题解,正解是用\(Kruscal\)最小生成树算法做的,然后因为我不会\(Kruscal\)所以这里直接贴一波\(std\)叭!

\(std\) \(Code\)

#include
#include
#include
#include
#include
#include
#include
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(!isdigit(c))
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(isdigit(c))
    {
        a=(a<<3)+(a<<1)+(c^48);
        c=getchar();
    }
    return a*b;
}
int num,s,t,maxn,father[5005];
struct edge
{
    int from,to,dis;
}e[400005];
bool cmp(edge a,edge b)
{
    return a.dis

##其实有T4,但是是一个毒瘤高精+动规,所以劳资不写啦!!!

你可能感兴趣的:(5 7 级 错 位 打 ♂ 击 赛)