[贪心][二分]Occupy the Cities 2021CCPC桂林站G

23 篇文章 0 订阅
22 篇文章 0 订阅

JB is playing a game. There are nn cities in the game, numbered as 1,2,⋯,n1,2,⋯,n. The ii-th city and the jj-th city are adjacent if and only if i=j−1i=j−1 or i=j+1i=j+1. Initially, some of the cities are occupied by JB.

The game runs in rounds. At the beginning of a round, each occupied city can mark at most one adjacent unoccupied city as the target of attack. At the end of the round, all the attack targets marked become occupied. The game ends when all the cities are occupied.

JB wants to occupy all the cities in minimum rounds. Can you help him?

Input

There are multiple test cases. The first line of the test case contains a positive integer TT, indicating the number of test cases. For each test case:

The first line contains an integer nn (1≤n≤1061≤n≤106), indicating the number of cities.

The next line contains a string ss of length nn. It's guaranteed ss only contains '0' and '1'. The ii-th character describes the initial state of the ii-th city: if si=si= '1', the ii-th city is occupied by JB initially. Otherwise, the ii-th city is not occupied initially.

It's guaranteed that the sum of nn over all the test cases doesn't exceed 106106. It's also guaranteed that there is at least one '1' in ss.

Output

For each test case, output one line, containing the minimum number of rounds to occupy all the cities.

Example

input

5
3
010
4
0100
7
0001000
5
11111
6
010101

output

2
2
4
0
1

Note

For the second test case, the best way is 0100→0110→11110100→0110→1111.

题意: 有n座城市排成一横排,其中某些城市开始时被攻占,每座被攻占的城市可以在一个时间单位内攻占下其左侧或右侧未被攻占的城市,不过只能选择一侧,不可以同时攻占两侧城市,给出n座城市初始状态,求所有城市都被攻占的最少时间。

分析: 这道题目有两种做法,分别是二分和贪心做法,比赛的时候没想到二分,就一直在想贪心的做法,后来没想到真的用贪心解决了。

先考虑O(nlogn)的二分解法,由于攻占时间具有单调性,所以在最外层可以二分时间,对于确定的时间mid只需要判断是否能在mid时间内将全部城市攻占,判断的过程可以O(n)遍历,对于每个0的位置判断一下它被攻占需要的时间,如果所有0被攻占的时间都小于等于mid,那么就return true,否则return false,0被攻占所花费的时间可以分两部分,一部分是被其左边的1攻占需要的时间,一部分是被其右边的1攻占需要的时间,二者取min即为该0被攻占需要的时间,不过在求被其左右1攻占的时间时还是比较麻烦的,首先需要维护出哪些位置是单独的1,还需要每个0左侧和右侧第一个1的位置信息,因为每个0肯定都是被其左或右第一个1所攻占,如果当前这个0左侧第一个1是连续的1,那不需要特殊处理,如果是单独的1那需要判断一下是否需要让它向右攻占,只有当向右攻占后能在mid时刻攻占该0,那我们才让它向右攻占,之后按相同的思路对右侧第一个1求出一个攻占时间,需要注意的是如果一个1已经被标记为向左攻占了,那么之后就不能再让它向右攻占了,另外这里为什么先看左侧再看右侧?因为我们是从左向右遍历0的,贪心地想肯定尽量用左边的1,这样不会对下一段0产生影响。

之后看下O(n)的贪心解法,首先可以想到如果1全部都是连续成段的,那么答案就是每一段连续的0的个数+1再除2,然后对每段都取max,两侧连续的0需要时间比较特殊,是0的个数,但是一开始时可能会有一些单独的1,第1秒后这些单独的1也会构成一段连续的1,所以第2秒就可以确定最终答案了,但是我们并不确定第1秒这些单独的1会向左扩还是向右扩,不过很显然扩完以后应该让每段0的个数尽量接近,这样取max值以后才会最小,所以单独的1应该向0个数多的那边扩展,因此需要统计每个单独的1左右两侧连续0的个数,在统计前应该先把一些一定会变成1的位置更新,比如一段连续的1那么其两端在一个时间单位后一定会向两侧扩展1格,而两端点如果有1,那么一个时间单位后一定会向s[2]或者s[n-1]扩展,将这些确定的1填完后就可以for循环遍历了,对于每个独立的1就看其左右0个数,要注意由于我们会扩展单独的1,所以某个单独的1其左右0的个数是在动态更新的,当两侧0的个数相同时应该向左侧扩展,否则遇到像10000100010001这样的情况就会出错。

具体代码如下: 

二分做法:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <string>
#define inf 0x3f3f3f3f
using namespace std;

char s[1000005];
int n, L[1000005], R[1000005];//Li,Ri记录两侧第一个1的位置 
bool flag[1000005];//第i个数是否为单独的1 
int LorR[1000005];//单独的1向左还是右进行扩展 

bool check(int x){
	for(int i = 1; i <= n; i++) LorR[i] = 0;
	for(int i = 1; i <= n; i++){
		if(s[i] == '0'){
			int l_t = i-L[i]+1, r_t = R[i]-i+1;
			if(L[i] != -inf && !flag[L[i]]) l_t--;
			if(R[i] != inf && !flag[R[i]]) r_t--;
			if(min(l_t, r_t) <= x) continue;
			if(l_t-1 == x && L[i] != -inf && flag[L[i]] && LorR[L[i]] == 0){
				LorR[L[i]] = 1;
				l_t--;
				continue; 
			} 
			if(r_t-1 == x && R[i] != inf && flag[R[i]] && LorR[R[i]] == 0){
				LorR[R[i]] = 2;
				r_t--;
				continue;
			}
			return false;
		}
	}
	return true;
}

signed main()
{
	int T;
	cin >> T;
	while(T--){
		scanf("%d%s", &n, s+1);
		int pos = -1;
		for(int i = 1; i <= n; i++){
			if(s[i] == '1') pos = i;
			else L[i] = (pos==-1?-inf:pos);
			flag[i] = false;
		}
		pos = -1;
		for(int i = n; i >= 1; i--)
			if(s[i] == '1') pos = i;
			else R[i] = (pos==-1?inf:pos);
		for(int i = 1; i <= n; i++)
			if(s[i] == '1' && i-1 >= 1 && s[i-1] == '0' && i+1 <= n && s[i+1] =='0')
				flag[i] = true;
		int l = 0, r = n+10, ans = -1;
		while(l <= r){
			int mid = l+r>>1;
			if(check(mid)){
				ans = mid;
				r = mid-1;
			} 
			else l = mid+1;
		}
		if(ans != -1) printf("%d\n", ans);
	} 
    return 0;
}

贪心做法:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
int cnt, n, l[N], r[N]; 
char s[N];
bool pos[N];

void solve(){
	cnt = 0;
	scanf("%d%s", &n, s+1);
	bool flag = false;
	for(int i = 1; i <= n; i++){
		pos[i] = false;
		l[i] = r[i] = 0;
	}
	for(int i = 1; i <= n; i++){
		if(s[i] == '1' && i+1 <= n && s[i+1] == '0' && i-1 >= 1 && s[i-1] == '0')
			pos[i] = true;
		if(s[i] == '0') flag = true;
	}
	if(!flag){
		puts("0");
		return;
	}
	int n1 = 0, start = -1;
	for(int j = 1; j <= n; j++){
		if(s[j] == '1'){
			n1++;
			if(start == -1) start = j;
		}
		else{
			if(n1 >= 2){
				s[start-1] = '1';
				s[j] = '1';
			}
			start = -1;
			n1 = 0;
		}
	}
	if(n1 >= 2){
		s[start-1] = '1';
		start = -1;
	}
	if(s[1] == '1') s[2] = '1';
	if(s[n] == '1') s[n-1] = '1';
//		printf("%s\n", temp+1);
	int n0 = 0;
	flag = false;
	for(int i = n; i >= 1; i--){
		if(s[i] == '0') n0++;
		else{
			if(pos[i]){
				r[i] = n0;
				if(!flag) r[i] *= 2;
			}
			n0 = 0;
			flag = true;
		}
	}
	n0 = 0;
	flag = false;
	for(int i = 1; i <= n; i++){
		if(s[i] == '0') n0++;
		else{
			if(pos[i]){
				if(!flag) n0 *= 2;
				if(n0 < r[i]) s[i+1] = '1';//如果两边相同,必须优先左边 
				else s[i-1] = '1';
//				if(n0 > r[i]) s[i-1] = '1';
//				else s[i+1] = '1';
			}
			n0 = 0;
			flag = true;
		}
	}
//		printf("%s\n", temp+1);
	int _max = 0, num = 0;
	n0 = 0;
	for(int j = 1; j <= n; j++){
		if(s[j] == '0')
			n0++;
		else{
			num++;
			if(num == 1)
				_max = max(_max, n0);
			else
				_max = max(_max, (n0+1)/2);
			n0 = 0;
		}
	}
	_max = max(_max, n0);
	_max++;
	printf("%d\n", _max);
	
}
int main(){
	int _;
	cin>>_;
	while(_--) solve();
	return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值