8.11 模拟赛记录

本文是作者对一次算法模拟赛的复盘,详细分析了比赛中遇到的四个题目。T1通过翻转序列和遍历解决,T2使用二维坐标系和最外圈图形维护策略,T3尝试用并查集和倍增LCA,但未能完成,T4因不熟悉二维树状数组导致未能解答。作者反思了在线做题策略和思路僵化的问题,并分享了赛后看到的DP解决方案。
摘要由CSDN通过智能技术生成

8.11 模拟赛记录

复盘

惯例开局遍历。
T1简单的我有点不敢想象,把一个数移到前面,等同于在前面复制一遍这个数,并忽略掉后面那个。为了方便,翻转这个序列,每次把要移的数移动到最后。移动都结束后,从后往前遍历,每一种数取最靠后出现的输出。时间复杂度O(m+n),完全能过。
考虑到T1如此简单,不如拿来当缓冲,于是先往下看。T2好像是小根堆,但是维护策略想不出来,先放弃。T3看到两个特殊限制,一个并查集可以解决,另一个倍增LCA,但是不太好写。T4第一反应二维树状数组前缀和暴力,但是二维树状数组我不熟悉,也不太好写,于是还是决定回头做T1。这时候是8:30左右。
T1不到20分钟轻松加愉快结束了,然后鉴于时间充裕,打算考虑一下能否写出T3的正解,但是各种各样的思路都试了一遍,并不能突破2000的瓶颈,还是止步于200.就这样一直卡到大约9:40,觉得必须开始写了(T4肯定得留至少一个小时),于是直接奔着60分部分分去了。为了同时判断和做两个子任务,我把并查集和倍增LCA强行写到了一起,毕竟两者本身不冲突。经过一系列缝合和复杂的子任务判定,自己出了个数据调了调,大约就快到11:00了,于是结束此题,直接做T4。
不得不说我对树状数组掌握实属不行,二维树状数组卡了特别长的时间在调试,一直到交卷的那一刻都没能做完。
预估分数:100+0+60+0.

复盘分析

得分:100+0+0+0.
对于T3,我觉得很可能是我子任务的判断有问题,导致两个一起贪又都没得分。而且最窒息的地方在于,T3明明是可以离线先判断子任务后做的,但是我不知道怎么想的愣是写成了在线算法,于是炸了。别的没啥可说的,T1白给,T2就是不会,T4二维树状数组写挂了,也只能说是回去好好复习了。总归今天的节奏一言难尽,说保守吧,T3敢在线做贪两个特殊数据,不保守吧,时间强制控制的非常合理。虽然100分在今天基本上和爆零就没有区别,但总之分数上说得过去,不过T3爆零了就真的难受。
以及T4我赛后去看洛谷上的题解,有人说可以dp,我没看代码就恍然大悟,不到十分钟打了个dp交上去测,有O2的情况下能得高达70分。所以说,思路还是应该活一点,大概是T4树状数组调不出来导致思维也跳不出来了。

题解

T1没啥可讲的。
T2就比较神奇了。
这题想求三种物品最大值和最小,如果暴力就把每个点看成一个空间直角坐标系里的点,看每个点为对角的时候与原点所成长方体是否能覆盖所有的点。要优化的话,肯定要想办法去掉一维,那这里就可以去掉a。去掉a很简单,由于要每种物品最大价值,把a降序排序,如果选择了a[i],a[i]之后所有点都可以扔进a集合,只有前面的需要扔进bc集合。把bc集合里的点抽象成平面直角坐标系里的点,显然我们需要维护一个最外圈的图形把剩下的点全部包括在内。很显然这个最外圈的图案要做到上面的每一个点都做到b比自己大的c比自己小,c比自己大的所有的点b比自己小,反之亦然,说白了就是在c-b图像上呈一个台阶状。
在这里插入图片描述
如图所示,淡蓝色的点就是可取的答案。我们只需要随着遍历,每次向图上加点,并维护这个台阶形就可以了。
假设新的点c不大于右边的第一个点,这个点是没用的,因为绝对被右边的覆盖,可以直接扔了;如果不小于左边的第一个点,则可以把左边的点连接的边删了,改为连这个点,把左边的点放在图形内部,直到小于左边的为止;如果正好在左右两点之间,让左右两点连上这个点,把原来连左右两点的线删掉(即扩展了图形)。
为此,我们利用set维护图中的点,以b为第一关键字排序;利用multiset维护淡蓝色点的横纵坐标之和,其第一个值就是本次的答案。更新的过程和上面那一段的做法是一样的。
这个思路很不好想到,实现起来也比较恶心人。核心代码如下:

const int mod = 1e9 + 7;
typedef pair<int,int> pr;
typedef set<pair<int,int> > Setpair;
//中间部分省略
long long solve(){
	Setpair St;
	multiset<int> Multi; 
	int i;
	long long ret = buc[1].a;//初始化
	Setpair::iterator next,last,now;
	St.insert(make_pair(mod,0));
	St.insert(make_pair(0,mod));//防止越界
	Multi.insert(0);
	for(i = 1;i <= n;i++){//指的是前i个数存进bc集合
		pr P(buc[i].b,buc[i].c);
		if(St.count(P)) continue;
		St.insert(P);
		next = St.find(P);
		last = next++,now = last--;//一顿折腾,总之最后是last-now-next
		if((*now).second <= (*next).second) St.erase(now);
		else{
			Multi.insert((*now).first + (*next).second);
			Multi.insert((*now).second + (*last).first);
			Multi.erase(Multi.find((*last).first + (*next).second));
			//先加上,然后判断
			while((*now).second >= (*last).second){
				--last,--now,--next;//指针整体左移
				Multi.erase(Multi.find((*now).second + (*last).first));
				Multi.erase(Multi.find((*now).first + (*next).second));
				Multi.insert((*last).first + (*next).second);
				St.erase(now);//重新取p的下标,减少思考量
				now = St.find(P);
				last = now,--last;
				next = now,++next;
			}
		}
		ret = min(ret,(long long)buc[i + 1].a + *Multi.begin());
	}
	return ret;
}

T3其实有一种办法可以直接不用并查集,而全部用倍增LCA上树解决。
如果一份文件能从a传到b,有两个条件:b是a的祖先且b和a在传递前是联通的。前者用一个LCA就可以,后者则仿照倍增的思路求一下b到a之间最大的时间,进行一波判断就可以,这个求法就是设time[i][j]表示j向上跳2^i步的路径上最大的时间,维护方式与st表一样,查找的时候枚举i跳就可以了。
完整代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
const int N = 150001;
struct yjx{
	int nxt,to,c;
}e[N << 1];
struct fxj{
	int deliver,tie;
}p[N << 1];
struct wyx{
	int cnt,tim,receiver;
}q[N << 1];
int tot,ecnt = -1,head[N],in[N],time[21][N],fa[21][N],dep[N],dfn[N],siz[N];
int pcnt,qcnt;
void save(int x,int y,int w){
	e[++ecnt].nxt = head[x];
	e[ecnt].to = y;
	e[ecnt].c = w;
	head[x] = ecnt;
	++in[y];
}
void dfs(int now,int f){
	int i,temp;
	dfn[now] = ++tot,dep[now] = dep[f] + 1,siz[now] = 1;
	fa[0][now] = f;
	for(i = 1;(1 << i) <= dep[now];i++){
		fa[i][now] = fa[i - 1][fa[i - 1][now]];
		time[i][now] = max(time[i - 1][now],time[i - 1][fa[i - 1][now]]);
	}
	for(i = head[now];~i;i = e[i].nxt){
		temp = e[i].to;
		if(temp == f) continue;
		time[0][temp] = e[i].c;
		dfs(temp,now);
		siz[now] += siz[temp];
	}
	return;
}
bool judge(int x,int y){
	if(dfn[x] <= dfn[y] && dfn[y] <= dfn[x] + siz[x] - 1) return 1;
	return 0;
}
int lca(int x,int y){
	int i;
	if(dep[x] < dep[y]) swap(x,y);
	if(judge(y,x)) return y;
	for(i = 18;i >= 0;i--){
		if(fa[i][x] && !judge(fa[i][x],y)) x = fa[i][x];
	}
	return fa[0][x];
}
int Getime(int from,int to){
	int i,ret = 0;
	for(i = 18;i >= 0;i--){
		if(fa[i][from] && dep[fa[i][from]] >= dep[to]){
			ret = max(ret,time[i][from]);
			from = fa[i][from];
		}
	}
	return ret;
}
signed main(){
	//freopen("manage.in","r",stdin);
	//freopen("manage.out","w",stdout);
	memset(head,-1,sizeof(head));
	int n,m,i,x,y,z,end;
	scanf("%lld %lld",&n,&m);
	for(i = 1;i <= m;i++){
		scanf("%lld",&z);
		if(z == 1){
			scanf("%lld %lld",&x,&y);
			save(y,x,i);
		}
		if(z == 2){
			scanf("%lld",&x);
			p[++pcnt].deliver = x,p[pcnt].tie = i;
		}
		if(z == 3){
			scanf("%lld %lld",&x,&y);
			q[++qcnt].receiver = x,q[qcnt].cnt = y,q[qcnt].tim = p[q[qcnt].cnt].tie;
			//这一句有点绕,x要收到y文件的时间就是y被发出的时间
		}
	}//离线
	for(i = 1;i <= n;i++){
		if(!in[i]) save(0,i,0);
	}
	dfs(0,0);
	for(i = 1;i <= qcnt;i++){
		x = p[q[i].cnt].deliver,y = q[i].receiver;
		end = lca(x,y);
		if(end != y){//相当于判断y不是x的祖先
			printf("NO\n");
			continue;
		}
		//printf("%lld %lld\n",Getime(x,y),q[i].tim);
		if(Getime(x,y) >= q[i].tim) printf("NO\n");
		else printf("YES\n");
	}
	return 0;
}

T4首先简单讲一句dp的做法,设dp[i][j]表示(i,j)为右下角最大的正方形边长。如果这格是X,dp[i][j]=0;不然(i,j)的状态只能由(i-1,j-1) (i,j-1) (i-1,j)三个点转移而来,显然应该取三者中最小的一个+1作为答案。
对于正解,随着X变多,答案一定不增。不增不好考虑,于是倒着考虑,每次去掉一个X,答案可能增,这个答案覆盖的正方形一定包括了去掉的X所在的点。所以倒着做这道题,维护每个点向上和向下能延伸的最大距离,每次去掉一个X的时候就把X所在的行更新,然后在X上枚举左端点,用单调队列处理右端点,维护区间最小上下距离,要求左右的距离必须在小于上下距离的时候才能扩展。队列当中存的上下距离是递增的,每次对于新的右端点都更新一下队列,并把这个右端点插入队列当中。如果左端点将要增加,且队头也等于左端点,就把队头后移。这样就维护出了答案。
核心代码:

void update(int x){
	int i;
	for(i = 1;i <= n;i++){
		if(s[i][x] == 'X') up[i][x] = 0;
		else up[i][x] = up[i - 1][x] + 1;
	}
	for(i = n;i >= 1;i--){
		if(s[i][x] == 'X') down[i][x] = 0;
		else down[i][x] = down[i + 1][x] + 1;
	}
}
int solve(int x){
	int i,ret = 0,pos = 1,Qu[2002],Qd[2002],frontu = 1,rearu = 0,frontd = 1,reard = 0;
	for(i = 1;i <= m;i++){
		while(pos == i || pos - i <= up[x][Qu[frontu]] + down[x][Qd[frontd]] - 1){
			//printf("%d %d %d\n",pos,i,up[x][frontu] + down[x][frontd] - 1);
			ret = max(ret,pos - i);
			//此处之所以不是pos-i,可以认为这里是加点-移动序列-更新答案的操作,pos已经+1了,只不过第一次不是这样而已
			if(pos > m) break;
			while(rearu >= frontu && up[x][Qu[rearu]] >= up[x][pos]) --rearu;
			Qu[++rearu] = pos;
			while(reard >= frontd && down[x][Qd[reard]] >= down[x][pos]) --reard;
			Qd[++reard] = pos;
			++pos;
		}
		if(Qu[frontu] == i) ++frontu;
		if(Qd[frontd] == i) ++frontd;
	}
	return ret;
}

Thank you for reading!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值