2021 BNU Winter Training 1 (NWERC-2018)

2021 BNU Winter Training 1 (NWERC-2018)

训练网址

A. Access Points

  • 题意:
    在这里插入图片描述
  • 思路:
  • 对于横坐标和纵坐标来说是两个独立的子问题,分别求解相加即可得到答案。
  • 于是问题转化成了:已知序列 A 1 , A 2 . . . A n A_1 , A_2 . . . A_n A1,A2...An,找到一个单调递增的序列 X 1 , X 2 . . . X n X_1 , X_2 . . . X_n X1,X2...Xn,使得 ∑ i = 1 n ( A i − X i ) 2 \sum^{n}_{i=1} (A_i-X_i)^2 i=1n(AiXi)2 最小,求这个最小值。
  • 通过观察我们可以得到两条性质:
  1. 假如所有X都相同,那么当X是A的平均数时,整个式子的值最小(二次函数的极值问题,展开求解即可);
  2. 假如A是单调递减的,那么最优解的所有X取值一定相同(反证法可证)。
  • 把序列的一些segment合并,使得成为一个单调上升的序列。合并规则为:从前往后扫描,若前面的segment的平均值比后面的segment的平均值大,那么就把这两个segment合并。这么做为什么是对的呢?我们可以用反证法,如果不合并的话,那么由性质2知,一定不如合并后的答案小。由于单调栈存的是pair,保存了数的个数信息,所以两两合并和把很多数一块儿合并是一样的结果。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

typedef long long ll;

typedef pair<ll, int> P;
const int maxn = 100010;

int x[maxn], y[maxn], N;
P stk[maxn];
double sum(int a[]) {
    
    
	int tt = 0;
	for (int i = 1; i <= N; i++) {
    
    
		ll nums = a[i];
		int cnt = 1;
		//stk[tt].first / stk[tt].second >= nums / cnt,分母移到两边
		while (tt && stk[tt].first * cnt >= nums * stk[tt].second) {
    
    
			nums += stk[tt].first;
			cnt += stk[tt].second;
			tt--;
		}
		stk[++tt] = {
    
     nums, cnt };
	}
	
	int k = 1;
	double res = 0;
	for (int i = 1; i <= tt; i++) {
    
    
		ll nums = stk[i].first;
		int cnt = stk[i].second;
		for (int j = k; j < k + cnt; j++) {
    
    
			res += pow(a[j] - (double)nums / (double)cnt, 2);
		}
		k += cnt;
	}
	return res;
}
int main() {
    
    
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) {
    
    
		scanf("%d%d", &x[i], &y[i]);
	}
	printf("%.10f\n", sum(x) + sum(y));
}

B. Brexit Negotiations

  • 题意:有n个会议要开,一段时间只能开一个会议。每个会议有两个要素:会议时间和前置会议。当前会议必须在前置会议结束后才能开。每个会议的时间 = 当前会议时间 + 第几个会议
  • 思路:有一个很自然的想法是,我们让时间最长的会议尽可能早开一些。如果我们反向拓扑排序,就可以在度数为0的节点中,优先把时间最短的会议挑出来(反向嘛,也就是说时间最短的会议放在最后)。手画一个图,发现确实可以把时间较长的会议放到尽可能早的时间。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> P;
const int maxn = 400010, maxm = 1000010;
int h[maxn], e[maxm], ne[maxm], idx;
int N, din[maxn], w[maxn];
void add(int a, int b) {
    
    
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
priority_queue<P, vector<P>, greater<P>> que;
int toposort() {
    
    
	int res = 0, tot = N;
	for (int i = 1; i <= N; i++) {
    
    
		if (!din[i]) que.push({
    
     w[i], i });
	}
	while (que.size()) {
    
    
		auto p = que.top(); que.pop();
		int u = p.second;
		tot--;
		res = max(res, tot + w[u]);
		
		for (int i = h[u]; i != -1; i = ne[i]) {
    
    
			int v = e[i];
			din[v]--;
			if (!din[v]) que.push({
    
     w[v], v });
		}
	}
	return res;
}
int main() {
    
    
	scanf("%d", &N);
	memset(h, -1, sizeof h);
	for (int i = 1; i <= N; i++) {
    
    
		int m;
		scanf("%d%d", &w[i], &m);
		for (int j = 0; j < m; j++) {
    
    
			int x;
			scanf("%d", &x);
			add(i, x);
			din[x]++;
		}
		
	}
	printf("%d\n", toposort());
	return 0;
}

C. Circuit Board Design

  • 题意:给定一个树,将其画在二维平面上,要求边不能交叉,边的欧几里得长度固定为 1,点之间不能离得太近。
  • 我这个题,是把圆按照子树的大小分为若干份,然后从中心向外扩展。对于每一个结点,让子结点开始旋转。其实这个题按照这个把圆分成若干份的做法,容易保证点与点之间的距离,但是两个区域的边界,边与边容易相交。可以看看代码中的注释,只要分配的空间略小一些就可以避免边与边相交的情况。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>

#define x first
#define y second

using namespace std;
const int maxn = 1010, maxm = 2010;
const double PI = acos(-1);


typedef pair<double, double> P;

int h[maxn], e[maxm], ne[maxm], idx;
int N, sz[maxn];
P ans[maxn];

void add(int a, int b) {
    
    
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int get_tree_size(int u, int fa) {
    
    
	sz[u] = 1;
	for (int i = h[u]; i != -1; i = ne[i]) {
    
    
		int v = e[i];
		if (v == fa) continue;
		sz[u] += get_tree_size(v, u);
	}
	return sz[u];
}

//根节点序号,,根节点的父结点序号,根节点坐标,根节点方向极角,分配的角度
void build(int u, int fa, P p, double C, double alpha) {
    
    
	if (sz[u] == 1) return;

	//为了防止相邻的两个子树的区域,边界的点离得很近。
	//不可以 2 * alpha / (sz[1] - 1.0), 得是 alpha / (sz[1] - 1.0)
	double phi = alpha / (double)(sz[u] - 1);
	C -= alpha / 2;
	for (int i = h[u]; i != -1; i = ne[i]) {
    
    
		int v = e[i];
		if (v == fa) continue;
		double theta = phi * sz[v];

		ans[v] = {
    
     p.x + cos(C + theta / 2), p.y + sin(C + theta / 2) };
		build(v, u, ans[v], C + theta / 2, theta);
		C += theta;
	}
}

int main() {
    
    

	scanf("%d", &N);
	memset(h, -1, sizeof h);
	for (int i = 1; i < N; i++) {
    
    
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
	}
	//求一下每个子树的大小
	get_tree_size(1, -1);

	ans[1] = {
    
     0, 0 };

	//我这种做法,基本上可以保证点与点之间距离不会太近,但会导致相邻的两个子树的区域,边界的点离得很近。
	//因此,不可以 2 * PI / (sz[1] - 1.0), 得是 PI / (sz[1] - 1.0)
	double phi = PI / (sz[1] - 1.0);
	double C = 0;
	for (int i = h[1]; i != -1; i = ne[i]) {
    
    
		//按照子树大小分配角度空间.
		int v = e[i];
		double theta = phi * sz[v];
		ans[v] = {
    
     cos(C + theta / 2), sin(C + theta / 2) };
		build(v, 1, ans[v], C + theta / 2, theta);
		C += theta;
	}
	for (int i = 1; i <= N; i++) {
    
    
		printf("%.10f %.10f\n", ans[i].x, ans[i].y);
	}
	return 0;
}

D. Equality Control

  • 题意:给出两个表达式,表达式中包含 [], concat, sort, shuffle。即序列,拼接,排序,随机。问这两个表达式的序列的出现概率是否都是对应相等的。
  • 这个题思路很简单,就是把所有数字提出来,然后看 shuffle 对应的区间是否相等。
  • 思路看注释,很详细。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;

typedef pair<int, int> P;
typedef pair<vector<int>, vector<P>> res;
const int maxn = 1000010;
char a[maxn], b[maxn];

res solve(char s[]) {
    
    
	int n = strlen(s);
	vector<int> nums, cnt(n, -1);
	//首先,先把所有整数提取出来放在nums里面
	for (int i = 0; i < n; i++) {
    
    
		if (s[i] < '0' || s[i] > '9') continue;
		int x = 0;
		while (s[i] >= '0' && s[i] <= '9') {
    
    
			x = x * 10 + s[i] - '0';
			i++;
		}
		i--;
		nums.push_back(x);
		//cnt != -1 时表示 s[i] 是该整数的最后一个数字,且 cnt[i] 为在 nums 中的下标。
		cnt[i] = nums.size() - 1;
	}

	/*
	pos存储的是shuffle的左右区间在 nums 中的下标。
	因为按照题目要求,只要shuffle后两个序列相等的概率不为0就算相等。
	shuffle后两个数组仍然相等,就要满足:
	(1)排序后,元素一一相等
	(2)shuffle的左右区间在 nums 中的下标也要对应相等
	当然,如果shuffle中所有元素都是一样的,那么这个shuffle就可以忽略,没有起到左右
	这里需要注意的是,对于起作用的shuffle,内部有无嵌套shuffle都一样,s(s(1, 2),3)和s(1, 2, 3)对应概率都相等。
	*/
	vector<P> pos;

	for (int i = 0; i < n; i++) {
    
    
		//保存过 nums 后,concat 没有意义。

		if (s[i] != 's') continue;

		//flag = true 意味着当前最外层操作为 shuffle
		bool flag = false;
		if (s[i + 1] == 'h') flag = true;

		while (s[i] != '(') i++;
		i++;  //定位到 ( 后面的位置。

		int c = 1, L = -1, R = -1;
		//c 表示内部嵌套了几个括号。
		//下面寻找最外面的这个shuffle或sorted对应的区间的第一个和最后一个数字,在nums中的下标是多少
		while (c) {
    
    
			if (s[i] == '(') c++;
			else if (s[i] == ')') c--;
			if (cnt[i] != -1) {
    
    
				if (L == -1) L = cnt[i];
				R = cnt[i];
			}
			i++;
		}
		i--;
		//回到上一个 ( ,防止越界。
		sort(nums.begin() + L, nums.begin() + R + 1);
		/*
		nums[L] != nums[R] 不能少,因为如果概率是 100% 的话(数字都一样)
		那么一个有shuffle,一个无shuffle,会判为不一样。
		当然,如果内部数字不一样,为了保证出现的概率对应相等,则shuffle对应下标的位置也必须相等。
		*/
		if (flag && nums[L] != nums[R]) pos.push_back({
    
     L, R });
	}
	return {
    
     nums, pos };
}
int main() {
    
    
	scanf("%s%s", a, b);
	if (solve(a) == solve(b)) printf("equal\n");
	else printf("not equal\n");
	return 0;
}

E. Game Design

  • 题意:给定一些操作符"LRUD",表示往某一个方向走直到遇到障碍。现在给定终点(0,0),要求构造一个起点以及若干障碍物,使得最后小球能够到达终点。注意必须是最后一步到达终点,中间到达终点即不合法。
  • 首先处理终点与起点的问题。这个题从后往前构造不如从前往后构造简单。所以我们假设起点是 (0, 0) ,最后走到终点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),那么把所有点的坐标都 ( x − x 0 , y − y 0 ) (x - x_0, y - y_0) (xx0,yy0),就ok了。
  • 然后,考虑构造问题。其实很简单,只要不碰见 “LRLR…, UDUD…” 两种情况,每次移动距离 d 都 + 10,然后把木块儿放在前方就可以。而上述两种情况,移动距离不增加即可,最后把木块儿去重就行。
  • 最后,处理 impossible 情况,其实发现中间的步骤中,不管怎么走似乎都是可以构造出来的,只有最后出现 “LRL, RLR, UDU, DUD”,木块儿是摆不出来的。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;

typedef pair<int, int> P;

int main() {
    
    
	string s;
	cin >> s;
	int n = s.length();

	//处理无解情况
	if (n >= 3 && s[n - 3] == s[n - 1]) {
    
    
		if (s[n - 2] == 'R' && s[n - 1] == 'L' || s[n - 2] == 'L' && s[n - 1] == 'R'
			|| s[n - 2] == 'U' && s[n - 1] == 'D' || s[n - 2] == 'D' && s[n - 1] == 'U') {
    
    
			puts("impossible");
			return 0;
		}
	}
	
	int x = 0, y = 0, d = 10;

	vector<P> ans;

	for (int i = 0; i < n; i++) {
    
    
		//模拟小球滚动和放木块儿的过程
		//x += d 这种是不可以的。我估计原因应该是,应该让小球离中心越来越远,让小球直接跳到某个位置。
		//而这种 x += d 的,画图会发现,走一个z字形之类的在原路返回时,会撞到之前的木块儿上。
		if (s[i] == 'L') {
    
    
			x = -d;
			ans.push_back({
    
     x - 1, y });
		}
		else if (s[i] == 'R') {
    
    
			x = d;
			ans.push_back({
    
     x + 1, y });
		}
		else if (s[i] == 'U') {
    
    
			y = d;
			ans.push_back({
    
     x, y + 1 });
		}
		else {
    
    
			y = -d;
			ans.push_back({
    
     x, y - 1 });
		}
		//d 在不会发生来回滚动的情况下才增加
		if (i + 1 < n) {
    
    
			if (s[i] == 'R' && s[i + 1] == 'L' || s[i] == 'L' && s[i + 1] == 'R'
				|| s[i] == 'U' && s[i + 1] == 'D' || s[i] == 'D' && s[i + 1] == 'U') continue;
			d += 10;
		}
	}

	sort(ans.begin(), ans.end());
	ans.erase(unique(ans.begin(), ans.end()), ans.end());
	
	printf("%d %d\n", -x, -y);
	printf("%d\n", ans.size());
	for (auto p : ans) {
    
    
		printf("%d %d\n", p.first - x, p.second - y);
	}
	return 0;
}

F. Jinxed Betting

  • 题意:给定一些数,每次可以对这些数进行操作,一次操作为:设最大的数有 a 个,将最大的 ⌊ a 2 ⌋ \lfloor \frac{a}{2} \rfloor 2a个数,以及其他所有非最大的数均加 1。再给定一个目标 t,问至多操作几次,可以保证这些数中的最大者不超过 t.
  • 思路:这个就是分类讨论,设最大数 x 1 x_1 x1 a 1 a_1 a1 个,次大数 x 2 x_2 x2 a 2 a_2 a2 个;可以发现,如果次大数赶上最大数需要的时间是 ( x 1 − x 2 ) ∗ ( l o g 2 a 1 + 1 ) (x_1-x_2)*(log_2a_1+1) (x1x2)(log2a1+1),而最大数赶上 t 的时间是 x 0 − x 1 + ( x 0 − x 1 ) / v x_0 - x_1 + (x_0 - x_1) / v x0x1+(x0x1)/v.
  • 那么我们可以分三种情况讨论:
  1. 当前只剩下一种数字,即只剩下最大数 x 1 x_1 x1,可以把答案直接加上
  2. x 1 x_1 x1 赶上 t t t 的时间比 x 2 x_2 x2 赶上 x 1 x_1 x1 的时间要短,可以把答案直接加上
  3. x 1 x_1 x1 赶上 t t t 的时间比 x 2 x_2 x2 赶上 x 1 x_1 x1 的时间要长,需要将最大数和次大数合并
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
typedef long long ll;
using namespace std;
const int maxn = 100010;
typedef pair<ll, ll> P;
#define x first
#define y second

P stk[maxn];
ll a[maxn];
int N;


int main() {
    
    
	scanf("%d", &N);
	for (int i = 0; i < N; i++) scanf("%lld", &a[i]);
	sort(a, a + N);

	int tt = 0;
	stk[++tt] = {
    
     a[0], 1 };
	for (int i = 1; i + 1 < N; i++) {
    
    
		if (stk[tt].x == a[i]) stk[tt].y++;
		else stk[++tt] = {
    
     a[i], 1 };
	}

	ll ans = 0, x0 = a[N - 1];


	while (1) {
    
    
		ll x1 = stk[tt].x, a1 = stk[tt].y;
		
		ll u = (int)log2(a1) + 1, v = (int)log2(a1);
		ll m = 0;

		if (v != 0) m = (x0 - x1) / v;

		ll t1 = x0 - x1 + m; 

		//(1)当前只剩下一种数字,即只剩下最大数 x1
		if (tt == 1) {
    
    
			ans += t1;
			break;
		}

		//这里需要注意,x2应该加上ans,即现在经过的时间,因为 stk[tt - 1] 的答案并没有更新
		ll x2 = stk[tt - 1].x + ans, a2 = stk[tt - 1].y;
		ll t2 = (x1 - x2) * u;

		//(2)x1 赶上 t 的时间比 x2 赶上 x1 的时间要短
		if (v && t1 <= t2) {
    
    
			ans += t1;
			break;
		}
		//(3)x1 赶上 t 的时间比 x2 赶上 x1 的时间要长
		else {
    
    
			ans += t2;
			auto p1 = stk[tt--], p2 = stk[tt--];
			P p = {
    
     p2.x + ans, p1.y + p2.y };
			stk[++tt] = p;
		}

	}
	printf("%lld\n", ans);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_45812711/article/details/113824016