Educational Codeforces Round 18

教育场 = 教你编程的场次 or 教你做人的场次? 比赛时完成前4题. E题是一个需要一点小技巧/脑洞的数学题, F的标签是 data structures, geometry, 好像是在凸壳上搞事, 至今只有一人AC. 由于是unrated, 右边栏没有下降的分值表影响心情, 又由于是教育场, 不分pretest和system test, 状态稍微好一些. 题解: 5/6 为了节省版面, 下面的代码把宏和using指令省略了.

A. New Bus Route

数轴上有n个不同的点, 给出它们的坐标, 求两点间最小距离和距离最小的点的对数. 距离定义为坐标之差的绝对值. (2≤n≤2*10^5, -109≤ai≤109)

距离最小的两点一定是相邻的, 排序即可.

const int N = 2e5;
int a[N];

int main()
{
	int n;
	cin >> n;
	Rep (i, 0, n) cin >> a[i];
	sort(a, a+n);
	int mn = 2e9 + 1, cnt = 0;
	Rep (i, 1, n) {
		if (a[i] - a[i-1] < mn) {
			mn = a[i] - a[i-1];
			cnt = 0;
		}
		if (a[i] - a[i-1] == mn)
			++cnt;
	}
	cout << mn << " " << cnt << endl;
	return 0;
}

B. Counting-out Rhyme

n个小朋友从1开始编号围成一圈, 进行k轮游戏. 一开始1是领袖. 第i轮, 领袖顺时针数的第ai个人 (包括自己) 离开, 并钦定离开者的下一位当领袖. 求每轮离开的人的编号. (2≤n≤100, 1≤k≤n-1, 1≤ai≤10^9)

教育场不求题目新颖, 而要凸显教育意义. 本题即约瑟夫问题的变种. 由于n很小, 模拟即可, 但是ai可以很大, 所以对圈内人数取个模.

const int N = 100;

int a[N], pre[N + 1], nxt[N + 1];

int main()
{
	int n, k;
	cin >> n >> k;
	Rep (i, 0, k) cin >> a[i];
	For (i, 1, n) {
		pre[i] = i-1;
		nxt[i] = i+1;
	}
	pre[1] = n;
	nxt[n] = 1;
	int s = 1;
	Rep (i, 0, k) {
		a[i] %= n-i;
		while (a[i]--)
			s = nxt[s];
		printf("%d%c", s, " \n"[i == k-1]);
		nxt[pre[s]] = nxt[s];
		pre[nxt[s]] = pre[s];
		s = nxt[s];
	}
	return 0;
}

C. Divide by Three

一个所含数字不超过10^5的十进制数, 去掉个数最少的数字 (可以为0), 使得新数不含前导0且能被3整除 (可以为0), 求一种方案. 无解输出-1.

比赛时我的做法比Editorial暴力, 但是思路简单一些. 首先一个数能被3整除当且仅当数位和能被3整除. 计算数位和. 特判去掉0个数字的情况. 接下来需要判断能否去掉一些数字, 它们的数位和等于现在的数位和. 去掉模等于0的数是没有意义的, 除非是去掉其他的数导致产生了前导0. 模等于1或2的数, 每种去掉多于2个也是没有意义的. 所以, 枚举一下, 从低位往高位删数字 (尽量避免产生前导0), 再删去高位的0. 最后特判得到0的情况.

通过一点讨论, 可以减小常数. 特判去掉0或1个数字的情况. 如果还没搞定, 一定是只有模3等于1的数, 或者只有模3等于2的数. 而(2+2) mod 3 = 1, (1+1) mod 3 = 2, 所以仍有希望搞定. 可以像上面那样从低位往高位贪心地删数字, 不是很明白Editorial在做什么.

const int L = 1e5;
string s, ans;
bool f[L + 1];

int get(int x, int y)
{
	int cnt = 0, first = s.size();
	fill_n(f, s.size(), false);
	Down (i, s.size()-1, 0) {
		if (x && (s[i]-'0')%3 == 1)
			--x;
		else if (y && (s[i]-'0')%3 == 2)
			--y;
		else
			f[i] = true, ++cnt, first = i;
	}
	if (x || y) return 0;
	while (first < (int)s.size() && s[first] == '0') {
		f[first++] = false;
		--cnt;
		while (first < (int)s.size() && !f[first])
			++first;
	}
	return cnt;
}

int main()
{
	ios::sync_with_stdio(false);
	
	cin >> s;
	int sum = 0;
	Rep (i, 0, s.size())
		(sum += s[i] - '0') %= 3;
	if (sum == 0) {
		cout << s << endl;
		return 0;
	}

	For (i, 0, 2) For (j, 0, 2)
		if ((i + j*2) % 3 == sum && get(i, j) > (int)ans.size()) {
			ans.clear();
			Rep (k, 0, s.size())
				if (f[k])
					ans.push_back(s[k]);
		}
	if (!ans.size()) {
		if (s.find('0') < s.size())
			cout << "0" << endl;
		else
			cout << "-1" << endl;
	} else
		cout << ans << endl;
	return 0;
}

D. Paths in a Complete Binary Tree

用中序遍历给n个结点的完全二叉树 (叶子全在同一层的满二叉树) 编号, q个询问, 每次给一个初始位置和一个序列 (Up, Left, or Right), 问结束位置在哪里, 忽略不合法的操作. (1≤n≤10^18, 序列的长度之和不超过10^5)

以前研究过这个问题, 但是不够深入啊. 一种紧凑的线段树存储方法 (后来并没有使用这种方法)

我用栈维护了一下根到当前结点的结点序列和子树大小, 所有选择右子树的结点, 模拟了一发.

Editorial给的方法是bitmask. 观察到实际上有:

inline ll right_child(ll x)
{
	return x + (lowbit(x) >> 1);
}

inline ll left_child(ll x)
{
	return x - (lowbit(x) >> 1);
}

inline ll parent(ll x)
{
	ll t = lowbit(x);
	return (x & (t << 1)) ? x - t : x + t;
}

我的模拟:

typedef long long ll;
const int D = 70;
ll v[D], sz[D];
int top = 0;
stack<ll> R;

inline void go_up()
{
	if (top == 1) return;
	--top;
	if (R.top() == v[top-1]) R.pop();
}

inline void go_left()
{
	if (sz[top-1] == 1) return;
	sz[top] = (sz[top-1]-1)/2;
	v[top] = R.top() + (sz[top]-1)/2 + 1;
	++top;
}

inline void go_right()
{
	if (sz[top-1] == 1) return;
	R.push(v[top-1]);
	sz[top] = (sz[top-1]-1)/2;
	v[top] = R.top() + (sz[top]-1)/2 + 1;
	++top;
}

int main()
{
	ios::sync_with_stdio(false);
	ll n, q;
	cin >> n >> q;
	R.push(0);
	while (q--) {
		ll u;
		string s;
		cin >> u >> s;
		top = 1;
		v[0] = (n+1)/2;
		sz[0] = n;

		while (v[top-1] != u) {
			if (u < v[top-1]) go_left();
			else go_right();
		}

		Rep (i, 0, s.size()) {
			switch (s[i]) {
			case 'U':
				go_up(); break;
			case 'L':
				go_left(); break;
			case 'R':
				go_right(); break;
			}
		}

		cout << v[top-1] << endl;

		while ((int)R.size() > 1) R.pop();
	}
	return 0;
}

E. Colored Balls

有n种颜色的球, 每种求ai个. 将它们分组, 使得: 所有球属于某个组, 不存在空组, 每组球的颜色相同, 任意两组元素个数之差的绝对值不超过1. 求最少组数. (1≤n≤500, 1≤ai≤10^9)

已知一组的最小规模, 每种颜色的最优分组是确定的. 设某种颜色的球有个, 一组的最小规模为, 分为组, 那么 解之, 得 由于, 中必有一个不大于. 枚举之.

枚举的方法是随便找一个, 转而枚举. 令. 当, 枚举; 否则, 枚举.

typedef long long ll;
const int N = 500;
const ll inf = 1e9 * 500LL + 1;
int n;
ll a[N];

ll cal(ll x)
{
	ll ans = 0;
	Rep (i, 0, n) {
		ll l = (a[i]+x)/(x+1), r = a[i]/x;
		if (l <= r) ans += l;
		else return inf;
	}
	return ans;
}

int main()
{
	ios::sync_with_stdio(false);
	
	ll ans = inf, mn = inf;
	cin >> n;
	Rep (i, 0, n) {
		cin >> a[i];
		mn = min(mn, a[i]);
	}

	for (ll x = 1; x*x <= mn; ++x)
		ans = min(ans, cal(x));

	for (ll k = 1; k*k <= mn; ++k) {
		ll x = mn/k;
		ans = min(ans, cal(x));
		if (mn % k == 0 && x > 1)
			ans = min(ans, cal(x - 1));
	}

	cout << ans << endl;
	
	return 0;
}