AtCoder Beginner Contest 254 G,H

G
下面“换乘”的定义是从一栋楼的某层到另一栋楼的相同一层,题中说这样耗时为1

首先如果Yi>Wi,那么我们不妨将XiZi以及YiWi交换顺序。此时我们就假定出发的楼层一定低于(或相等)于到达的楼层。

有一个显然的结论:

在最优路线中,我们只能朝上走,不会朝下走。

通过这个显然的结论可以得到一个显然的推论:

答案等于高差(WiYi)加上最少换乘电梯次数。

前面这个是常量,所以我们只关系最少换乘电梯的次数。

貌似这个可以用倍增解决,比如设f(i,j)表示从i层出发,换乘2j次最高可以到达的层数。

然后一开始和最终都要换乘楼房,所以还要将答案加上2

但这样有一个问题——我们最终乘坐的电梯和最优解是同一个,也就意味着最后我们没必要换乘楼房,那么此时就不能加上2;同理,一开始我们乘坐的电梯可能和最优解相同。

如何解决?显然要将出发、到达和中间的电梯分开考虑。那么此时我们需要另一个观察:

开始时,我们尽量不要离开这栋楼,也就是,如果有往上的电梯,那么我们就一直往上。
结束前,我们尽早到达最终的楼房,然后再达电梯一路向上。

大致就是下图的流程:

这样,我们只统计中间最少需要的换乘次数,再加上2即可。

时间复杂度为O(nlogn),注意要将楼层离散化。

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

#define x1 x114514
#define x2 x1919810
#define y1 y114514
#define y2 y1919810

const int maxn=1000005,maxlog=22;

int n,m,q;
int a[maxn],b[maxn],c[maxn];
int x1[maxn],x2[maxn],y1[maxn],y2[maxn];
std::map<int,int> cnt[maxn];

int mx[maxn],nxt[maxn][maxlog];
std::set<std::pair<int,int>> S[maxn]; 

using iter=std::set<std::pair<int,int>>::iterator;

int main() {
	scanf("%d%d%d",&n,&m,&q);
	std::vector<int> numbers;
	for(int i=1;i<=m;i++) {
		scanf("%d%d%d",&a[i],&b[i],&c[i]);
		numbers.push_back(b[i]);
		numbers.push_back(c[i]);
	}
	for(int i=1;i<=q;i++) {
		scanf("%d%d%d%d",&x1[i],&y1[i],&x2[i],&y2[i]);
		if(y1[i]>y2[i]) {
			std::swap(x1[i],x2[i]);
			std::swap(y1[i],y2[i]);
		}
		numbers.push_back(y1[i]);
		numbers.push_back(y2[i]);
	}
	std::sort(numbers.begin(),numbers.end());
	numbers.resize(std::unique(numbers.begin(),numbers.end())-numbers.begin());
	auto number=[&](int ori)->int{
		return std::lower_bound(numbers.begin(),numbers.end(),ori)-numbers.begin()+1;
	};
	for(int i=1;i<=m;i++) {
		cnt[a[i]][b[i]]++;
		cnt[a[i]][c[i]]--;
	}
	for(int i=1;i<=n;i++) {
		if(cnt[i].empty()) continue;
		int cur=0,lef=-1;
		for(auto item : cnt[i]) {
			if(lef==-1) lef=item.first;
			cur+=item.second;
			if(!cur) {
				int rig=item.first;
//				debug(lef); debug(rig);
				S[i].insert({lef,rig});
				lef=number(lef),rig=number(rig);
				mx[lef]=std::max(mx[lef],rig); 
				lef=-1; 
			}
		}
	}
	for(int i=0;i<(int)numbers.size();i++) {
		//real number : i+1	
		nxt[i+1][0]=std::max({i+1,nxt[i][0],mx[i+1]});
	}
	for(int i=1;i<maxlog;i++) {
		for(int j=1;j<=(int)numbers.size();j++) {
			nxt[j][i]=nxt[nxt[j][i-1]][i-1];
		}
	}
	for(int i=1;i<=q;i++) {
		//求start,end,from,to
		int start=y1[i],end=y2[i],from,to;
		//找from和to,如果找不到,那from=start,to=end(防止x1[i]=x2[i],y1[i]=y2[i]这种情况) 
		iter it;
		it=S[x1[i]].lower_bound({start+1,-1});
		if(it==S[x1[i]].begin()||(--it)->second<start) {
			from=start;
		} else {
			from=it->second;
		}
		it=S[x2[i]].lower_bound({end+1,-1});
		if(it==S[x2[i]].begin()||(--it)->second<end) {
			to=end;
		} else {
			to=it->first;
		}
		//debug(start); debug(end); debug(from); debug(to);
		if(number(to)<=number(from)) {
			//有交集,直接换乘 
			printf("%d\n",end-start+(x1[i]!=x2[i]));
			continue;
		}
		int floor=number(to),cnt=0,pos=number(from);
		for(int j=maxlog-1;~j;j--) {
			if(nxt[pos][j]<floor) {
				pos=nxt[pos][j];
				cnt+=1<<j;
			}
		}
		cnt++; pos=nxt[pos][0];
		if(pos<floor) {
			printf("-1\n"); continue;
		}
		cnt++;//换乘,注意由于之前我们统计的是搭乘电梯数,此时的cnt已经加上了1 
		//默认 end > start 否则交换 
		printf("%d\n",end-start+cnt);
	}
	return 0;
}

这题程序很难写对(H也一样),所以下面给几个样例:

Sample 1:
2 1 3
1 232 333
1 222 1 222
1 222 2 222
2 333 2 232
Answer :
0
1
103

Sample 2:
3 5 5
1 1610 1720
1 1710 1780
2 1380 1650
3 1550 1570
3 1640 1730 
1 1380 1 1780
3 1640 2 1780
1 1580 2 1700
1 1720 1 1730
2 1730 3 1570
Answer:
402
142
123
10
163

H
自己想了一个单log的做法,应该比题解快吧。

通常这样乘二和除二下取整的,我们可以使用Trie。

在这题中,我们对A数组的每个数创建0-1Trie,记为TrieA。但与平时不同,我们建立Trie的时候,不能加入前导0。比如对于Sample A的A数组,我们构建如下的Trie:

在这个Trie上,我们对某个数乘二就相当于在其末尾加一个0。比如样例中A数组,将A1=32后,Trie就会变成这个样子:

在这个Trie上,我们对某个数除以二下取整,就相当于将某尾一位删掉,比如样例中A数组,将A1=3除以2下取整后,Trie就会变成这个样子:

在这个Trie上,我们肯定先进行除以二下取整的操作,再进行乘二操作。(否则你先乘二,之后除以二相当于每进行任何操作)那么我们对一个数进行变换,就相当于将这个节点往祖先走若干步,再向左儿子(也就是数位为0的边,相当于乘二)走几步。比如将A1先除以二,再乘以二,就相当于在Trie上进行如下变换:

那么,我们也按相同的方法对B构建TrieB,那么我们就是对TrieA进行上面的操作,使其和TrieB相同,并要使得操作尽可能少。

首先我们考虑什么时候会是1

此时我们可以得到一个充要条件:

对于某个点,如果我们在TrieA中有a个点在其右子树中,在TrieB中有b个点在其右子树中,并且a<b,则无解。

因为在右子树中意味着这一位为1,那么在右子树外的点一定不能通过×2进入右子树中(因为乘二是在某尾加上0,没法在某尾加上1),所以我们右子树的点只能进行内部的匹配,那如果TrieA中的点不够TrieB需要的,自然就无解了。并且容易发现这个条件是充分的。(之前我以为是ab无解,导致wa了9个点,反例见后文下发样例二)

那么剩下的情况就合法了。但如何方便的统计答案?

可以发现,假设对于某个点,我们TrieB中需要q个数在这个子树中,而TrieA中有p个,那么我们需要移进(p<q)或者移出(p>q) Abs(pq)个数就可以了。

那么可以对Trie上每个点分别统计贡献,时间复杂度为O(nlogmax(A,B))

具体的判断1和统计答案的程序如下:

void match(int pos1,int pos2) {
	if(!pos1&&!pos2) return;
	int al=trieA.val[pos1].lson,ar=trieA.val[pos1].rson;
	int bl=trieB.val[pos2].lson,br=trieB.val[pos2].rson;
	if(br&&trieA.val[ar].siz<trieB.val[br].siz) {
		printf("-1\n");
		exit(0);
	}
	ans+=(ll)abs(trieA.val[pos1].siz-trieB.val[pos2].siz);
	match(al,bl); match(ar,br);
}

整体程序如下,还是很简洁的:

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

using ll=long long;

const int maxn=100005,maxlog=40;
int n;
ll a[maxn],b[maxn];
ll ans;

struct trie {
	struct node {
		int lson,rson;
		int siz,tag;
	}val[maxn*maxlog];
	int cur,root;
	int add() {
		cur++;
		val[cur].lson=val[cur].rson=0;
		val[cur].siz=val[cur].tag=0;
		return cur;
	}
	trie() {
		cur=0; root=add();
	}
	void insert(int pos,ll dig,const ll &num) {
		val[pos].siz++;
		if(dig<0) {
			return;
		}
		if(num&(1ll<<dig)) {
			if(!val[pos].rson) val[pos].rson=add();
			insert(val[pos].rson,dig-1,num);
		} else {
			if(!val[pos].lson) val[pos].lson=add();
			insert(val[pos].lson,dig-1,num);
		}
	}
	void flush(int pos) {
		if(!pos) return; 
		int L=val[pos].lson,R=val[pos].rson;
		flush(L); flush(R);
		if(R==0&&!val[L].tag) val[pos].tag=0;
		else val[pos].tag=1;
	}
}trieA,trieB;

void match(int pos1,int pos2) {
	if(!pos1&&!pos2) return;
	int al=trieA.val[pos1].lson,ar=trieA.val[pos1].rson;
	int bl=trieB.val[pos2].lson,br=trieB.val[pos2].rson;
	if(br&&trieA.val[ar].siz<trieB.val[br].siz) {
		printf("-1\n");
		exit(0);
	}
	ans+=(ll)abs(trieA.val[pos1].siz-trieB.val[pos2].siz);
	match(al,bl); match(ar,br);
}

int main() {
	scanf("%d",&n);
	for(int i=1;i<=n;i++) {
		scanf("%lld",&a[i]);
	}
	for(int i=1;i<=n;i++) {
		scanf("%lld",&b[i]);
	}
	for(int i=1;i<=n;i++) {
		ll limit=maxlog-1;
		while(limit>=0&&!(a[i]&(1ll<<limit))) limit--;
		trieA.insert(trieA.root,limit,a[i]);
	}
	for(int i=1;i<=n;i++) {
		ll limit=maxlog-1;
		while(limit>=0&&!(b[i]&(1ll<<limit))) limit--;
		trieB.insert(trieB.root,limit,b[i]);
	}
	trieA.flush(trieA.root);
	trieB.flush(trieB.root);
	match(trieA.root,trieB.root);
	printf("%lld\n",ans);
	return 0;
}

这题也很容易写假,所以给出下面的几个附加样例,供自测:

Extra test 1 :
4
0 1 2 9
0 1 1 9
Answer : 1

Extra test 2 :
6
19 19 21 23 25 31
18 23 25 64 72 120
Answer : 18
posted @   Nastia  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示