巨狗狗也AK了!

89 篇文章 0 订阅

原来的那条大狗狗长成了巨狗狗,令人类欣慰!

题目

题目背景
“嘘,小声点,为了防止有 人类 偷听我们的谈话。” 校长压低了声音,谨慎地观望了一下四周,然后接着说,“听说 T a Ta Ta g o o d good good 国派来的间谍,终极目的是要毁灭……!”

题目描述
人类 F i r e W i n g B i r d \sf FireWingBird FireWingBird 放置了一个摄像头,意在监视 狗狗 们的行动。

这个摄像头记录了某个地下据点的 狗狗 进出情况。不过有的 狗狗 会伪装起来,这样 F i r e W i n g B i r d \sf FireWingBird FireWingBird 就不知道那究竟是谁了。

现在给出这个摄像头的记录,分为以下两种:

  • E    x {\tt E}\;x Ex 表示一个编号为 x x x狗狗 进入了据点。如果 x = 0 x=0 x=0 则这位 狗狗 进行了伪装,身份不明。
  • L    x {\tt L}\;x Lx 表示一个编号为 x x x狗狗 离开了据点。如果 x = 0 x=0 x=0 则这位 狗狗 进行了伪装,身份不明。

现在 F i r e W i n g B i r d \sf FireWingBird FireWingBird 想知道,这个据点是否有其他出入口?如果答案是肯定的,输出 O T H E R \rm OTHER OTHER,否则输出最后在据点内的最小 狗狗 数量,方便 T a Ta Ta 毁灭……!

注意:据点内一开始可能是有 狗狗 在的。编号为 x    ( x ⩾ 1 ) x\;(x\geqslant 1) x(x1)狗狗 仅有一位。

数据范围与提示
监控记录的数量 n ⩽ 1 0 3 n\leqslant 10^3 n103,编号 x ⩽ 2 × 1 0 3 x\leqslant 2\times 10^3 x2×103 。数据组数 T ⩽ 10 T\leqslant 10 T10

提示:上救生艇时,妇女和儿童优先,这是全世界通用的良知。

思路

这个 n ⩽ 1 0 3 n\leqslant 10^3 n103 真的是一个败笔。但同时,它也是这道题 最大的陷阱

在这里给出第二个提示:数据范围一定要给到极限吗?时间复杂度其实是 O ( n log ⁡ 2 n ) \mathcal O(n\log^2n) O(nlog2n) 的!

然而看到 n ⩽ 1 0 3 n\leqslant 10^3 n103,开始脑补神奇做法:由于一些类似二分图匹配的原因,可以先假设每个人都最初就在据点内,然后删去一个人,看看是否仍然成立。并没有往 贪心 的方向想。而且事实上,只要会判断 O T H E R \rm OTHER OTHER,整道题就做完了。

到底为什么会 O T H E R \rm OTHER OTHER 呢?其实就是一个人 连续 两次进入据点,或者连续两次离开据点。而 E    L    E    L \tt E\;L\;E\;L ELEL 这样交错的序列不会导致任何问题。

也就是说,我们的目的是,用 E    0 {\tt E}\;0 E0 修正连续两次 L \tt L L,用 L    0 {\tt L}\;0 L0 修正连续两次 E \tt E E 。这就是我们的唯一目的。

那么,一个 E    0 {\tt E}\;0 E0 出现时,如果可以修正多个人的状态,该修正谁呢?这就要用到第一个提示给出的信息了:都有需要时,紧迫者优先!也就是说,当前不在据点中的人,谁的下一次 L \tt L L 到来最早,谁就应该先用 E \tt E E 摆脱困境!

完整的证明则非常简单。毕竟所有的 L - L \tt L{\text-}L L-L 都需要一个 E \tt E E,如果当前的 E \tt E E 去救别人,救别人的那一次不一定能救自己,反之却可行。

如果并没有 L - L \tt L\text-L L-L 型的人呢?假设进入一个接下来是 E \tt E E 操作的人,那么二者中间就需要一个 L \tt L L,必然是一次 L    0 {\tt L}\;0 L0 。这个方案可以转化为进来一个从没见过的人,然后又走掉了。而没有 L    0 {\tt L}\;0 L0 的情况甚至会导致 O T H E R \rm OTHER OTHER 发生!所以最优策略就是进入一个从来没见过的人。

L    0 {\tt L}\;0 L0 出现时,同理,首先找在据点中,且下一个操作是 E \tt E E 的人。如果找不到,就不像上面那样可以随便选一个。如果有接下来没有任何事干的人,让他出去不会导致任何问题;否则,本次 L \tt L L 一定会导致一个 L - L \tt L\text-L L-L 型诞生!

核心的贪心思想贯穿全题:紧急者优先。我们要制造麻烦,就一定要制造最不紧急的麻烦。也就是下一个 L \tt L L 操作最远的。直觉而言,这是无可置疑的;严谨证明还是考虑互换 trouble-solver \text{trouble-solver} trouble-solver E    0 {\tt E}\;0 E0 即可。

s e t set set 维护一下:在据点内且下一个操作是 E \tt E E 的人;在据点外且下一个操作是 L \tt L L 的人;在据点内的所有人。都按照下一次操作的时间排序。那么上述过程是 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 的。

突然发现一个问题。好像忘了据点内开始是有人的啊啊啊啊——

大丈夫です!据点内最开始的人不就 相当于 E    0 {\tt E}\;0 E0 吗?我们可以二分这个数量。显然我们只需要最小化据点内初始人数即可,因为答案就是初始人数 + + + E \tt E E 的数量 − - L \tt L L 的数量。

时间复杂度 O ( n log ⁡ 2 n ) \mathcal O(n\log^2n) O(nlog2n),诚然常数大,但是单组数据 n ⩽ 1 × 1 0 5 n\leqslant 1\times 10^5 n1×105 还是在 0.4 s 0.4s 0.4s 以内出结果(我自己随机了几组数据)。

代码

#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <set>
// #include <ctime>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(int x){
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}
inline void getMin(int &x,const int &y){
	if(y < x) x = y;
}

const int MaxN = 1000005;
int opt[MaxN], x[MaxN], n;

vector< pair<int,int> > v[MaxN]; // <time,option>
// 0: outside leaving  1: inside coming
set< pair<int,int> > ren[2], all; // <time,index>
int ziyou; // no more things to do
bool now[MaxN]; // if someone's inside
bool come(int x,int tim){
	if(x == 0){
		if(!ren[0].empty()){
			auto pii = *ren[0].begin();
			ren[0].erase(ren[0].begin());
			x = pii.second;
			all.insert(make_pair(v[x].back().first,x));
			return now[x] = true; // inside
		}
		else return ++ ziyou, true; // anybody
	}
	else{
		if(now[x]) return false;
		v[x].pop_back(); now[x] = true;
		if(!v[x].empty()){
			all.insert(make_pair(v[x].back().first,x));
			if(v[x].back().second) // inside coming
				ren[1].insert(make_pair(v[x].back().first,x));
		}
		else now[x] = 0, ++ ziyou;
		return true;
	}
}
bool leave(int x,int tim){
	if(x == 0){
		if(!ren[1].empty()){
			auto pii = *ren[1].begin();
			ren[1].erase(ren[1].begin());
			x = pii.second;
			all.erase(make_pair(v[x].back().first,x));
			return (now[x] = false), true;
		}
		else if(ziyou) return -- ziyou, true;
		else if(!all.empty()){
			auto pii = *all.rbegin();
			all.erase(pii); x = pii.second;
			ren[0].insert(make_pair(v[x].back().first,x));
			return (now[x] = false), true;
		}
		else return false;
	}
	else{
		if(!now[x]) return false;
		all.erase(make_pair(tim,x));
		v[x].pop_back(); now[x] = false;
		if(!v[x].empty() && !v[x].back().second) // outside leaving
			ren[0].insert(make_pair(v[x].back().first,x));
		return true; // carry through
	}
}
int check(int w){
	rep(j,0,1) ren[j].clear(); all.clear();
	rep(i,1,n) v[i].clear(); ziyou = 0;
	memset(now+1,0,n); // remember to clear!!!
	drep(i,n,1) if(x[i] != 0)
		v[x[i]].push_back(make_pair(i,opt[i]));
	rep(i,1,n) if(!v[i].empty()){
		pair<int,int> &pii = v[i].back();
		if(pii.second) continue; // outside want to come
		ren[0].insert(make_pair(pii.first,i));
	}
	rep(i,1,w) come(0,0);
	for(int i=1,f; i<=n; ++i){
		if(opt[i]) f = come(x[i],i);
		else f = leave(x[i],i);
		if(!f) return -1; // fail
	}
	int ans = ziyou;
	rep(i,1,n) ans += now[i];
	return ans;
}

const int infty = (1<<30)-1;
char char_gotter[5]; int tmp[MaxN];
int main(){
	for(int T=readint(); T; --T){
		n = readint();
		rep(i,1,n){
			scanf("%s",char_gotter);
			opt[i] = (*char_gotter == 'E');
			x[i] = readint();
			if(!x[i]) tmp[i] = 114514;
			else tmp[i] = x[i];
		}
		sort(tmp+1,tmp+n+1);
		int *sy = unique(tmp+1,tmp+n+1);
		rep(i,1,n) if(x[i] != 0)
			x[i] = lower_bound(tmp+1,sy,x[i])-tmp;
		if(check(n) == -1){
			puts("OTHER"); continue;
		}
		int L = 0, R = n, mid = R>>1;
		for(; L!=R; mid=(L+R)>>1)
			if(~check(mid)) R = mid;
			else L = mid+1;
		printf("%d\n",check(L));
	}
	return 0;
}

后记

最重要的事情:敢于贪心。以及瞪眼法发现 E    0 {\tt E}\;0 E0 L    0 {\tt L}\;0 L0用途与目的,有目的则可评估,可评估则可贪心。

以及考场上没想到这种做法时,应该像 人类 自己一样,写一个听上去就不能过打出来感觉不能过交上去认为过不了结果就过了的做法……

以及另外一个贪心做法,大体思路是,只要有一个框架就可以保证方案合法,只需要在框架内贪心调整即可。感觉合理,又感觉不合理,而造数据难度很高;大概是对的吧

尾声

当最后一片尘埃落下,一切归于死寂,人类 F i r e W i n g B i r d \sf FireWingBird FireWingBird 站在废墟之中,虽然入目皆硝烟, T a Ta Ta 还是感到一种抑制不住的激动。“终于,我们迎来了自由!从今天起,人类 不再任由 狗狗 号令,我们只听从自己的声音!”

同样的声浪从四方涌起。 F i r e W i n g B i r d \sf FireWingBird FireWingBird 满怀欣慰与骄傲地注视着一切。 T a Ta Ta 止不住地想起,一个帮助过 T a Ta Ta 的陌生面孔,用一个微不足道的 e x e \tt exe exe,算出了据点内的 狗狗 数量,让它成功地抓住了 狗狗 中最具威胁的校长!

F i r e W i n g B i r d \sf FireWingBird FireWingBird 坐上了 T a Ta Ta 的宝座。这个宝座也在闪闪发光,似乎在与 F i r e W i n g B i r d \sf FireWingBird FireWingBird 共同庆贺这场艰苦卓绝的战斗!

忽然, F i r e W i n g B i r d \sf FireWingBird FireWingBird 察觉到身下有异样,低头一看,宝座上浮现了一行字:

对不起, F i r e W i n g B i r d \sf FireWingBird FireWingBird,这次我帮人类。你不该这么轻易地坐上来的。—— H a n d I n D e v i l \sf HandInDevil HandInDevil

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值