Codeforces VK Cup 2015 - Round 1 (Div.1 A~E)

比赛地址

VK Cup 2015 - Round 1 (unofficial online mirror, Div. 1 only)


529A. And Yet Another Bracket Sequence

题意:

给定一个由'('和')'组成的括号序列,你可以对其添加'('或')',也可以将序列的末尾一部分接到前面,求将给定的括号序列变成的合法序列中字典序最小的,'('比')'字典序小。

序列长度 <= 100000。

题解:

一个括号序列是合法的当且仅当将'('看成1,')'看成-1后序列的任意前缀和均为非负。

添加操作可以将一段前缀和+1,剪切的操作即得到一个原串的循环串。

很明显如果整体的和为k,k > 0则应该添加恰好k个')',k < 0则应该恰好添加-k个‘(’,否则构造出的字符串长度一定不是字典序最小的答案。

由于数值的特性,算出原串的前缀和之后可以很容易求出任一循环串的前缀和,而且可以算出前缀和中的最小值。

例如将[1, n]的字符串变成[i, n][1, i - 1],则[i, n]的每一个前缀和增加定值k,[1, i - 1]的每一个前缀和减少k,所以可以通过计算每个点左边的前缀和最小值与右边的前缀和最小值从而得到循环串整体前缀和的最小值。

因为添加的字符对于循环串来说都是固定的,所以我们如果能算出满足添加括号之后前缀和均非负且字典序最小的循环串,则这个串一定为答案。

确定字典序最小的循环串可以利用后缀数组,可以构造两倍原串也可以直接使用循环串。

利用后缀数组得到按照字典序排序的循环串的编号sa[i]后,按顺序枚举每个字符串即可,可以发现一定有解。

设序列长度为n,时间复杂度为O(nlogn)。

一个可能的错误想法是求出字典序最小的循环串之后直接添加括号,注意字典序最小的循环串添加了括号后不一定是合法序列。

代码:

#include <cstdio>
#include <cstring>
const int maxn = 1000010;
char s[maxn];
int cnt, tran[256], len, x[maxn], y[maxn], c[maxn], sa[maxn], sum[maxn], l[maxn], r[maxn];
int min(int x, int y)
{
	return x < y ? x : y;
}
int main()
{
	scanf("%s", s);
	len = strlen(s);
	for(int i = 0; i < len; ++i)
	{
		if(!tran[s[i]])
		{
			++cnt;
			tran[s[i]] = 1;
		}
	}
	for(int i = 0, j = 0; i < 256; ++i)
		if(tran[i]) tran[i] = j++;
	for(int i = 0; i < len; ++i)
		++c[x[i] = tran[s[i]]];
	for(int i = 1; i < cnt; ++i)
		c[i] += c[i - 1];
	for(int i = len - 1; i >= 0; --i)
		sa[--c[x[i]]] = i;
	for(int k = 1; k <= len; k <<= 1)
	{
		int tmp = 0;
		for(int i = 0; i < len; ++i)
			y[tmp++] = (sa[i] + len - k) % len;
		memset(c, 0, sizeof c);
		for(int i = 0; i < len; ++i)
			++c[x[y[i]]];
		for(int i = 1; i < cnt; ++i)
			c[i] += c[i - 1];
		for(int i = len - 1; i >= 0; --i)
			sa[--c[x[y[i]]]] = y[i];
		memcpy(y, x, sizeof x);
		cnt = 1;
		x[sa[0]] = 0;
		for(int i = 1; i < len; ++i)
			if(y[sa[i - 1]] == y[sa[i]] && y[(sa[i - 1] + k) % len] == y[(sa[i] + k) % len]) x[sa[i]] = cnt - 1;
			else x[sa[i]] = cnt++;
		if(cnt >= len) break;
	}
	for(int i = 0; i < len; ++i)
		sum[i + 1] = sum[i] + (s[i] == '(' ? 1 : -1);
	l[0] = sum[0];
	for(int i = 1; i <= len; ++i)
		l[i] = min(l[i - 1], sum[i - 1]);
	r[len] = sum[len];
	for(int i = len - 1; i >= 0; --i)
		r[i] = min(r[i + 1], sum[i]);
	for(int i = 0; i < len; ++i)
	{
		int pos = sa[i];
		int now = min(r[pos] - sum[pos], sum[len] - sum[pos] + l[pos]);
		if(sum[len] > 0 && now >= 0)
		{
			for(int j = pos; j < len; ++j)
				putchar(s[j]);
			for(int j = 0; j < pos; ++j)
				putchar(s[j]);
			for(int j = 0; j < sum[len]; ++j)
				putchar(')');
			putchar('\n');
			break;
		}
		else if(sum[len] <= 0 && now - sum[len] >= 0)
		{
			for(int j = 0; j < -sum[len]; ++j)
				putchar('(');
			for(int j = pos; j < len; ++j)
				putchar(s[j]);
			for(int j = 0; j < pos; ++j)
				putchar(s[j]);
			putchar('\n');
			break;
		}
	}
	return 0;
}

529B. Group Photo 2 (online mirror version)

题意:

有n个矩形在地上排成一列,不可重叠,已知他们的宽度w_i和高度h_i,现在你可以使至多[n / 2]个矩形旋转90度,问最后可以用多小的矩形恰好覆盖这n个矩形,求满足条件的最小矩形面积。

n, w_i, h_i <= 1000。

题解:

数据范围比较小,可以枚举答案矩形的高度H,判断是否能只旋转至多[n / 2]个矩形使得n个矩形的高度均不超过H,再用剩下的操作次数尽量使得总宽度变少。

可以想到的是,尽量先考虑把w_i比h_i大很多的矩形旋转,所以可以对矩形按照{w_i - h_i}排序,贪心旋转即可。

设高度最大值为hmax,时间复杂度为O(nhmax)。

一个可能的错误想法是矩形按照w_i排序,这样不满足解最优,或者说可能使解变差。

代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1001, maxh = 1010;
int n, ans = ~0u >> 1;
struct Person
{
	int w, h;
	bool operator < (const Person &x) const
	{
		return w - h > x.w - x.h;
	}
} a[maxn], b[maxn];
int main()
{
	scanf("%d", &n);
	for(int i = 0; i < n; ++i)
		scanf("%d%d", &a[i].w, &a[i].h);
	for(int i = 1; i < maxh; ++i)
	{
		memcpy(b, a, sizeof a);
		bool flag = 0;
		int cnt = 0;
		for(int j = 0; j < n; ++j)
			if(b[j].h > i)
			{
				if(b[j].w > i)
				{
					flag = 1;
					break;
				}
				else
				{
					swap(b[j].w, b[j].h);
					++cnt;
				}
			}
		if(flag || cnt * 2 > n)
			continue;
		sort(b, b + n);
		for(int j = 0; j < n && (cnt + 1) * 2 <= n; ++j)
			if(b[j].w <= i && b[j].h < b[j].w)
			{
				swap(b[j].w, b[j].h);
				++cnt;
			}
		int tmp = 0;
		for(int j = 0; j < n; ++j)
			tmp += b[j].w;
		if(ans > tmp * i)
			ans = tmp * i;
	}
	printf("%d\n", ans);
	return 0;
}

529C. Rooks and Rectangles

题意:

给定一个n*m的象棋棋盘,上面有k个车,车的攻击范围是所在的行和列无阻碍能到达的地方,现在有q个询问,问一个矩形区域{(x1, y1), (x2,y2)}里所有的空格是否都能被至少一个车攻击到。

n, m <= 100000, k, q <= 200000。

题解:

询问可以看作询问矩形区域的行上是否有x2 - x1 + 1个不在同一行的车,或是矩形区域的列上是否有y2 - y1 + 1个不在同一列的车。

这两个情况可以分开考虑,且做法可以类似,现在考虑如何判断矩形区域的列上是否有y2 - y1 + 1个不在同一列的车。

这类似于询问一段区间里有多少个互不相同的数,不带修改,我们做这个问题的方法是离线地将询问和点按照左端点排序,每次一个数不再可能被询问到时,将其下一个出现的相同的数对询问的可能的贡献产生(+1),按顺序处理询问。

这题也可以类似来做,我们将点按照x坐标升序,询问[x1, y1, x2, y2]按照x2坐标升序,每次处理一个询问时,将x <= x2的点(x, y)按照升序加入贡献,维护已有的纵坐标为y的点里的x最大值。

由于是按照升序加入的贡献,所以不用求最大值而是直接使用赋值的形式维护。

此时只需要看在y1 <= y <= y2的点里每一个y维护的x的最大值,这些最大值一定<= x2,只需要看这之中最小的值是否已经>= x1,如果是,则表示每一列(y1 <= y <= y2)都至少添加了一个属于询问区域的点,则从列上看,这个询问区域是满足条件的。

所以我们处理列的时候对于x按升序排序,离线用线段树处理,需要维护的是单点赋值和区间最小值,处理行的时候则对于y按升序排序即可。

时间复杂度O((k + q)(logn + logm))。

一个可能的错误方法是按行维护每个列的使用情况,按列维护每个行的使用情况,这样无法通过区间合并快速计算。

代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxq = 200001, maxs = 262144;
int n, m, k, q, seg[maxs];
bool ans[maxq];
struct Point
{
	int x, y;
	bool operator < (const Point &t) const
	{
		return x < t.x;
	}
} rook[maxq];
struct Query
{
	int id, x1, y1, x2, y2;
	bool operator < (const Query &t) const
	{
		return x2 < t.x2;
	}
} query[maxq];
void upd(int o, int L, int R, int x, int val)
{
	if(L == R)
	{
		seg[o] = val;
		return;
	}
	int M = L + R >> 1;
	if(x <= M)
		upd(o + o, L, M, x, val);
	else
		upd(o + o + 1, M + 1, R, x, val);
	seg[o] = min(seg[o + o], seg[o + o + 1]);
}
int que(int o, int L, int R, int l, int r)
{
	if(l <= L && R <= r)
		return seg[o];
	int M = L + R >> 1;
	if(r <= M)
		return que(o + o, L, M, l, r);
	else if(l > M)
		return que(o + o + 1, M + 1, R, l, r);
	else
		return min(que(o + o, L, M, l, r), que(o + o + 1, M + 1, R, l, r));
}
int main()
{
	scanf("%d%d%d%d", &n, &m, &k, &q);
	for(int i = 0; i < k; ++i)
		scanf("%d%d", &rook[i].x, &rook[i].y);
	sort(rook, rook + k);
	for(int i = 0; i < q; ++i)
	{
		query[i].id = i;
		scanf("%d%d%d%d", &query[i].x1, &query[i].y1, &query[i].x2, &query[i].y2);
	}
	sort(query, query + q);
	for(int i = 0, j = 0; i < q; ++i)
	{
		while(j < k && rook[j].x <= query[i].x2)
		{
			upd(1, 1, m, rook[j].y, rook[j].x);
			++j;
		}
		ans[query[i].id] |= que(1, 1, m, query[i].y1, query[i].y2) >= query[i].x1;
	}
	memset(seg, 0, sizeof seg);
	swap(n, m);
	for(int i = 0; i < k; ++i)
		swap(rook[i].x, rook[i].y);
	sort(rook, rook + k);
	for(int i = 0; i < q; ++i)
	{
		swap(query[i].x1, query[i].y1);
		swap(query[i].x2, query[i].y2);
	}
	sort(query, query + q);
	for(int i = 0, j = 0; i < q; ++i)
	{
		while(j < k && rook[j].x <= query[i].x2)
		{
			upd(1, 1, m, rook[j].y, rook[j].x);
			++j;
		}
		ans[query[i].id] |= que(1, 1, m, query[i].y1, query[i].y2) >= query[i].x1;
	}
	for(int i = 0; i < q; ++i)
		puts(ans[i] ? "YES" : "NO");
	return 0;
}

529D. Social Network

题意:

一个社交网站丢失了一天内用户的信息,但是它保留了n条信息的出现时间,还知道在连续T秒时间内出现的人数最大值恰好为M,请你帮助管理员将每条信息所属的用户还原出来,同时管理员还希望用户数尽量地多,如果不存在一种解,输出"No solution",否则输出任意一组用户数最多的解即可。时间给出的格式为hh:mm:ss,保证输入顺序是按照时间非降序给出的。

n, M <= 20000, T <= 86400。

题解:

时间按照非降序给出我们则可以用一个队列来维护连续T秒时间内的人,或者说,处理到每一条信息时,维护这条信息在内的之前T秒时间的信息。

信息的时间是离散的点,所以队列空间是O(n)的。

判断是否有解,即是否存在一种状态使得队列里的元素个数达到M及以上。

对于用户尽量多,我们可以给出这样的策略:如果当前的人数+1(不是信息个数)没有超过M,则这条信息可以来自一位新增用户,如果当前的人数+1超过了M,则这条信息和上一条信息来自同一个用户,这样就可以成功构造出一组解。

队列也可以维护的是来自不同用户的最后一次信息,构造方法同上,时间复杂度O(n)。

直接树状数组维护也可以做,但是要注意可能有同一时间来自不同用户的信息,时间复杂度O(nlogT)。

代码:

#include <cstdio>
const int maxn = 20001;
int n, m, t, a[maxn], l, r, que[maxn], ans, out[maxn];
bool flag;
int main()
{
	scanf("%d%d%d", &n, &m, &t);
	for(int i = 0; i < n; ++i)
	{
		int hh, mm, ss;
		scanf("%d:%d:%d", &hh, &mm, &ss);
		a[i] = hh * 3600 + mm * 60 + ss;
		que[r++] = i;
		while(l < r && a[que[r - 1]] - a[que[l]] >= t)
			++l;
		if(r - l == m)
			flag = 1;qu'kuan
		if(r - l <= m)
			out[i] = ++ans;
		else
		{
			out[i] = out[que[r - 2]];
			que[r - 2] = que[r - 1];
			--r;
		}
	}
	if(flag)
	{
		printf("%d\n", ans);
		for(int i = 0; i < n; ++i)
			printf("%d\n", out[i]);
	}
	else
		printf("No solution\n");
	return 0;
}

529E. The Art of Dealing with ATM

题意:

ATM取款机有n种不同的钱币a_i,每次取款允许吐出不超过k张钱币,且钱币的种类数不能超过2,现在有q次取款,钱数为x_i,问ATM能否凑出这样的钱,若能的话吐出的钱币数最少是多少。

n <= 5000, k <= 20, q <= 20, a_i <= 10 ^ 7, x_i <= 2 * 10 ^ 8。

题解:

考虑用一种钱找零和用两种钱找零两种情况。

用一种钱找零可以直接枚举钱币,判断能否恰好找零,且钱数不超过k。

用两种钱找零可以写成a * x + b * y = k,其中x和y是两种钱币、a和b是两种钱币分别用的钱数、k是所需凑出的钱数,则最优的解法是枚举x, a, b,判断y是否存在。

答案即两种情况的钱币数最小值,其实第二种情况涵盖了第一种情况,时间复杂度O(nk^2q)。

一种错误的方法是枚举a, x, b, y,直接检查,另一种错误的方法是枚举x, y,扩展欧几里得找最小解,两种方法均会超时。

代码:

#include <map>
#include <cstdio>
using namespace std;
const int maxn = 5010;
int n, k, q, a[maxn], x, ans;
map<int, int> Hash;
int main()
{
	scanf("%d%d", &n, &k);
	for(int i = 0; i < n; ++i)
	{
		scanf("%d", a + i);
		Hash[a[i]] = 1;
	}
	scanf("%d", &q);
	while(q--)
	{
		ans = ~0u >> 1;
		scanf("%d", &x);
		for(int i = 0; i < n; ++i)
			for(int j = 0; j <= k; ++j)
			{
				if(x == a[i] * j)
					ans = min(ans, j);
				if(x < a[i] * j)
					break;
				for(int jj = 1; jj <= k - j; ++jj)
					if((x - a[i] * j) % jj == 0 && Hash.count((x - a[i] * j) / jj))
						ans = min(ans, j + jj);
			}
		if(ans > k)
			puts("-1");
		else
			printf("%d\n", ans);
	}
	return 0;
}

小记

这场比赛很容易fst……

你可能感兴趣的:(线段树,hash,后缀数组,贪心,单调队列)