北航校赛2014 决赛 题解

比赛地址

第十届北航程序设计竞赛现场决赛


A. jsy与他的物理实验

题意:

给定依次n个实验,每个实验有pi%概率获得ki点学分,修够27分就不再继续实验,问成功修够的话,期望要实验多少次。

n <= 10, ki <= 10, pi <= 100。

题解:

概率dp,f[i][j]表示做到第i个实验获得j学分的概率,最后统计一下所有可以修够学分的事件概率,算出成功实验的期望次数。

代码:

#include <cstdio>
#include <cstring>
const int maxn = 11, maxv = 27;
int n, cnt;
double f[maxn][maxv], p1, p2;
int main()
{
	while(scanf("%d", &n) != EOF)
	{
		cnt = 0;
		p1 = p2 = 0;
		memset(f, 0, sizeof f);
		f[0][0] = 1;
		for(int i = 1; i <= n; ++i)
		{
			int c, p;
			scanf("%d%d", &c, &p);
			if(p)
				cnt += c;
			for(int j = 0; j < maxv; ++j)
			{
				if(j + c >= maxv)
				{
					p1 += p / 100.0 * f[i - 1][j] * i;
					p2 += p / 100.0 * f[i - 1][j];
				}
				else
					f[i][j + c] += p / 100.0 * f[i - 1][j];
				f[i][j] += (100 - p) / 100.0 * f[i - 1][j];
			}
		}
		if(cnt < maxv)
			puts("-1");
		else
			printf("%.4f\n", p1 / p2);
	}
	return 0;
}

B. flappy bird

题意:

给定一个二维平面的flappy bird游戏,bird起初在(0, 0)处,现在垂直x轴有n个可以从中通过的柱子(可以蹭着边缘通过),但是每移动一单位需要耗费一点体力,问有m点体力的情况下最多通过几个柱子(可以是恰好通过)。注意移动的距离按欧几里得距离算。

n <= 1000, m <= 10^9, 坐标 <= 10^5。

题解:

视野型动态规划,最优路径一定是先走柱子的端点再横着通过某个柱子,这样一定最短,可以用一个橡皮筋模型来思考一下。

所以对于每个柱子,定义f[i][0]表示到达柱子下端点的最短路长度,f[i][1]表示到达柱子上端点的最短路长度,f[i][2]表示横着穿越该柱子停下的最短路长度。

而两个端点(或者直走)能转移的条件就是当前视野里能看到要到达的点,维护上下视野的向量即可,用叉积可以避免浮点精度误差地判断视野,时间复杂度O(n^2)。

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxm = 1010;
int n;
long long m, x[maxm], l[maxm], h[maxm];
double f[maxm][3];
inline long long det(long long x1, long long y1, long long x2, long long y2)
{
	return x1 * y2 - x2 * y1;
}
inline double dis(long long x, long long y)
{
	return sqrt(x * x + y * y);
}
int main()
{
	while(scanf("%lld%d", &m, &n) == 2)
	{
		x[0] = l[0] = h[0] = 0;
		for(int i = 1; i <= n; ++i)
		{
			scanf("%lld%lld%lld", x + i, l + i, h + i);
			f[i][0] = f[i][1] = f[i][2] = 2e9;
		}
		f[0][0] = f[0][1] = f[0][2] = 0;
		for(int i = 0, lpos, hpos; i < n; ++i)
		{
			lpos = i + 1, hpos = i + 1;
			for(int j = i + 1; j <= n; ++j)
			{
				if(det(x[j] - x[i], l[j] - l[i], x[lpos] - x[i], l[lpos] - l[i]) <= 0)
				{
					if(det(x[j] - x[i], l[j] - l[i], x[hpos] - x[i], h[hpos] - l[i]) >= 0)
						f[j][0] = min(f[j][0], f[i][0] + dis(x[j] - x[i], l[j] - l[i]));
					lpos = j;
				}
				if(det(x[j] - x[i], h[j] - l[i], x[hpos] - x[i], h[hpos] - l[i]) >= 0)
				{
					if(det(x[j] - x[i], h[j] - l[i], x[lpos] - x[i], l[lpos] - l[i]) <= 0)
						f[j][1] = min(f[j][1], f[i][0] + dis(x[j] - x[i], h[j] - l[i]));
					hpos = j;
				}
				if(det(x[lpos] - x[i], l[lpos] - l[i], x[hpos] - x[i], h[hpos] - l[i]) < 0)
					break;
				if(l[lpos] <= l[i] && l[i] <= h[hpos])
					f[j][2] = min(f[j][2], f[i][0] + x[j] - x[i]);
			}
			lpos = i + 1, hpos = i + 1;
			for(int j = i + 1; j <= n; ++j)
			{
				if(det(x[j] - x[i], l[j] - h[i], x[lpos] - x[i], l[lpos] - h[i]) <= 0)
				{
					if(det(x[j] - x[i], l[j] - h[i], x[hpos] - x[i], h[hpos] - h[i]) >= 0)
						f[j][0] = min(f[j][0], f[i][1] + dis(x[j] - x[i], l[j] - h[i]));
					lpos = j;
				}
				if(det(x[j] - x[i], h[j] - h[i], x[hpos] - x[i], h[hpos] - h[i]) >= 0)
				{
					if(det(x[j] - x[i], h[j] - h[i], x[lpos] - x[i], l[lpos] - h[i]) <= 0)
						f[j][1] = min(f[j][1], f[i][1] + dis(x[j] - x[i], h[j] - h[i]));
					hpos = j;
				}
				if(det(x[lpos] - x[i], l[lpos] - h[i], x[hpos] - x[i], h[hpos] - h[i]) < 0)
					break;
				if(l[lpos] <= h[i] && h[i] <= h[hpos])
					f[j][2] = min(f[j][2], f[i][1] + x[j] - x[i]);
			}
		}
		int ans = 0;
		for(int i = 0; i <= n; ++i)
			if(m >= min(f[i][2], min(f[i][0], f[i][1])))
				ans = i;
		printf("%d\n", ans);
	}
	return 0;
}

C. Kinfu的奥秘

题意:

给定一个初始长度为L,贴着三个坐标轴(正方向)的正方体,现在将正方体旋转平移放缩,给出新的八个顶点坐标,再询问原正方体里的某点对应新正方体的某点是多少。

所有数 <= 1000。

题解:

不妨选原正方体的三个基向量(1, 0, 0), (0, 1, 0), (0, 0, 1),看它们在坐标变换后变成了哪三个向量,直接按照新的基向量和原点来找新的点坐标。

代码:

#include <cstdio>
int L, a, b, c;
double x[8], y[8], z[8], xx, yy, zz;
int main()
{
	while(scanf("%d", &L) != EOF)
	{
		xx = yy = zz = 0;
		for(int i = 0; i < 8; ++i)
			scanf("%lf%lf%lf", x + i, y + i, z + i);
		scanf("%d%d%d", &a, &b, &c);
		x[1] -= x[0], x[2] -= x[0], x[4] -= x[0];
		y[1] -= y[0], y[2] -= y[0], y[4] -= y[0];
		z[1] -= z[0], z[2] -= z[0], z[4] -= z[0];
		xx = x[1] * a / L + x[2] * b / L + x[4] * c / L + x[0];
		yy = y[1] * a / L + y[2] * b / L + y[4] * c / L + y[0];
		zz = z[1] * a / L + z[2] * b / L + z[4] * c / L + z[0];
		printf("%.3f %.3f %.3f\n", xx, yy, zz);
	}
	return 0;
}

D. 这样还真是令人高兴啊

题意:

给定一个1~n的排列,支持两种操作,排列循环右移x位,求当前排列的逆序对数。m次操作。

n, m <= 10^5, x <= 10^9。

题解:

对于原排列,可以利用树状数组维护某些权值出现的次数的前缀和,来快速计算每个位和之前位产生的逆序对数,时间复杂度O(nlogn)。

总共只有n种不同的排列,考虑当前排列和循环右移一位的排列之间逆序对数的变化,即原来最后一位产生的逆序对消失,不是逆序对的变成逆序对,可以通过一次对树状数组的查询得到,单次时间复杂度O(logn),预处理出所有可能的排列的逆序对数时间复杂度为O(nlogn)。

然后就是维护当前循环右移了多少位即可。

代码:

#include <cstdio>
#include <cstring>
const int maxn = 100010;
int n, m, a[maxn], bit[maxn], delta;
long long f[maxn];
void add(int x)
{
	for( ; x <= n; x += x & -x)
		++bit[x];
}
int sum(int x)
{
	int ret = 0;
	for( ; x > 0; x -= x & -x)
		ret += bit[x];
	return ret;
}
int main()
{
	while(scanf("%d%d", &n, &m) == 2)
	{
		f[0] = delta = 0;
		memset(bit, 0, sizeof bit);
		for(int i = 0; i < n; ++i)
		{
			scanf("%d", a + i);
			add(a[i]);
			f[0] += i - sum(a[i] - 1);
		}
		for(int i = 1; i < n; ++i)
			f[i] = f[i - 1] + sum(a[n - i] - 1) * 2 - n + 1;
		while(m--)
		{
			char op[2];
			int x;
			scanf("%s", op);
			if(op[0] == 'Q')
				printf("%lld\n", f[delta]);
			else
			{
				scanf("%d", &x);
				delta = (delta + x) % n;
			}
		}
	}
	return 0;
}

E. 已经没有什么好怕的了

题意:

数轴上有n个点,每次可以选择一个位置x,将这个位置及其后面的k个位置(x, x + d, x + 2 * d, ..., x + (k - 1) * d)上的点删除,问最少要做几次删除操作才能删掉所有的点。

n <= 1000, 坐标, k, d <= 10^9。

题解:

按坐标升序依次枚举每个未删除的点,删除它并看后面能尽量删掉哪些点,时间复杂度O(n^2)。

代码:

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1001;
int n, k, d, x[maxn], ans;
int main()
{
	while(scanf("%d%d%d", &n, &k, &d) == 3)
	{
		ans = 0;
		for(int i = 0; i < n; ++i)
			scanf("%d", x + i);
		sort(x, x + n);
		for(int i = 0; i < n; ++i)
			if(x[i])
			{
				++ans;
				for(int j = i + 1; j < n; ++j)
					if(x[j] && (x[j] - x[i]) % d == 0)
						if((x[j] - x[i]) / d >= k)
							break;
						else
							x[j] = 0;
				x[i] = 0;
			}
		printf("%d\n", ans);
	}
	return 0;
}

F. 奇迹和魔法都是存在的

题意:

数轴上有n个点,坐标xi为自然数,现在要将它们移动到连续的n个整数的位置,每个点移动的代价是两个位置之间的距离,求代价之和的最小值。

n <= 1000, xi <= 10^6。

题解:

如果点按照升序排序,则可以枚举不动的那个点的位置,然后计算其他点移动到它附近的代价,判断即可,时间复杂度O(n^2)。

实际上,终态应该是{xi - i}相等的位置,则可以对xi排序,之后{xi - i}也一定是有序的,从新序列里取中位数作为中心直接计算代价即可,时间复杂度O(nlogn)。

代码:

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1001;
int n, a[maxn], pos, ans;
int main()
{
	while(scanf("%d", &n) != EOF)
	{
		for(int i = 0; i < n; ++i)
			scanf("%d", a + i);
		sort(a, a + n);
		for(int i = 0; i < n; ++i)
			a[i] -= i;
		pos = a[n >> 1];
		ans = 0;
		for(int i = 0; i < n; ++i)
			ans += a[i] <= pos ? pos - a[i] : a[i] - pos;
		printf("%d\n", ans);
	}
	return 0;
}

G. 这种事情我绝对不允许

题意:

给定一个分为n段的函数,问其和左边界、右边界、x轴之上所交的图形是否封闭,如果封闭,求其面积。

n <= 100, |坐标| <= 10^4。

题解:

将在x轴之上的部分的区间计算出来,在计算积分的同时判断是否存在间断点即可,细节题。

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
const int maxn = 101;
const double eps = 1e-12;
int n, l, r, a[maxn], b[maxn], c[maxn], s[maxn];
inline int dcmp(double x)
{
	if(fabs(x) < eps)
		return 0;
	return 0 < x ? 1 : -1;
}
inline double area(int id, double L, double R)
{
	return ((1.0 / 3 * a[id] * R + 1.0 / 2 * b[id]) * R + c[id]) * R - ((1.0 / 3 * a[id] * L + 1.0 / 2 * b[id]) * L + c[id]) * L;
}
inline double f(int id, double x)
{
	return (a[id] * x + b[id]) * x + c[id];
}
double lastx, lasty, ans;
bool Area(int id, double L, double R)
{
	if(!dcmp(lastx - L) && dcmp(lasty - f(id, L)))
		return 0;
	ans += area(id, L, R);
	lastx = R;
	lasty = f(id, lastx);
	return 1;
}
int main()
{
	while(scanf("%d%d%d", &n, &l, &r) == 3)
	{
		bool flag = 1;
		for(int i = 0; i < n; ++i)
			scanf("%d%d%d%d", a + i, b + i, c + i, s + i);
		ans = 0.0;
		lastx = l - 1;
		for(int i = 0; i < n; ++i)
		{
			int L = s[i], R = i == n - 1 ? r : s[i + 1];
			if(!a[i])
			{
				if(!b[i])
				{
					if(c[i] > 0)
						flag &= Area(i, L, R);
				}
				else
				{
					double x = -(double)c[i] / b[i];
					if(dcmp(f(i, L)) >= 0 && dcmp(f(i, R)) >= 0)
						flag &= Area(i, L, R);
					else if(dcmp(f(i, L)) > 0)
						flag &= Area(i, L, x);
					else if(dcmp(f(i, R)) > 0)
						flag &= Area(i, x, R);
				}
			}
			else
			{
				if(b[i] * b[i] - 4 * a[i] * c[i] <= 0)
				{
					if(a[i] > 0)
						flag &= Area(i, L, R);
				}
				else
				{
					double x1 = (-b[i] - sqrt(b[i] * b[i] - 4 * a[i] * c[i])) / (2 * a[i]), x2 = (-b[i] + sqrt(b[i] * b[i] - 4 * a[i] * c[i])) / (2 * a[i]);
					double RR = x1 < R ? x1 : R, LL = L < x2 ? x2 : L;
					if(a[i] > 0)//x1 < x2
					{
						if(dcmp(L - RR) < 0)
							flag &= Area(i, L, RR);
						if(dcmp(LL - R) < 0)
							flag &= Area(i, LL, R);
					}
					else//x1 > x2
					{
						if(dcmp(LL - RR) < 0)
							flag &= Area(i, LL, RR);
					}
				}
			}
			if(i)
			{
				double f1 = f(i - 1, s[i]), f2 = f(i, s[i]);
				if(dcmp(f1) >= 0 && dcmp(f2) >= 0 && dcmp(f1 - f2) || dcmp(f1) * dcmp(f2) < 0)
					flag = 0;
			}
			if(!flag)
				break;
		}
		if(!flag)
			puts("0.000");
		else
			printf("%.3f\n", ans);
	}
	return 0;
}

H. 再也不会依赖任何人了

题意:

给定一个长度为n的非负整数序列,有m个操作,操作有两种,将一段区间的每个数平方,求一段区间的每个数之和的平方模61的值。

n, m <= 10^5, 序列元素在int范围。

题解:

首先可以想到将所有数模61,在模意义下维护更加轻松。

而61是一个质数,对于小于61的自然数a,有a ^ 60 mod 61 = 1,则有a ^ 64 mod 61 = a ^ 4,或者说a ^ (2 ^ 6) mod 61 = a ^ (2 ^ 2)。

每个数的2次方幂存在循环节,所以可以在线段树上维护每个数x的x, x ^ 2, x ^ 4, x ^ 8, x ^ 16, x ^ 32,区间同理,则区间平方操作即为数组右移操作,区间求和则直接求就可以了,时间复杂度O(6mlogn)。

代码:

#include <cstdio>
#include <cstring>
const int maxn = 131072 << 1, mod = 61;
int n, m;
struct SegTree
{
	int sum[6], tag;
} seg[maxn];
int sqr(int x)
{
	return x * x % mod;
}
void push_up(int o)
{
	for(int i = 0, now1 = seg[o + o].tag, now2 = seg[o + o + 1].tag; i < 6; ++i)
	{
		seg[o].sum[i] = seg[o + o].sum[now1++] + seg[o + o + 1].sum[now2++];
		if(seg[o].sum[i] >= mod)
			seg[o].sum[i] -= mod;
		if(now1 >= 6)
			now1 = 2;
		if(now2 >= 6)
			now2 = 2;
	}
}
void push_down(int o)
{
	if(!seg[o].tag)
		return;
	seg[o + o].tag += seg[o].tag;
	if(seg[o + o].tag >= 6)
		seg[o + o].tag = (seg[o + o].tag - 2) % 4 + 2;
	seg[o + o + 1].tag += seg[o].tag;
	if(seg[o + o + 1].tag >= 6)
		seg[o + o + 1].tag = (seg[o + o + 1].tag - 2) % 4 + 2;
	seg[o].tag = 0;
}
void build(int o, int L, int R)
{
	if(L == R)
	{
		scanf("%d", &seg[o].sum[0]);
		seg[o].sum[0] %= mod;
		for(int i = 1; i < 6; ++i)
			seg[o].sum[i] = sqr(seg[o].sum[i - 1]);
		return;
	}
	int M = L + R >> 1;
	build(o + o, L, M);
	build(o + o + 1, M + 1, R);
	push_up(o);
}
void mul(int o, int L, int R, int l, int r)
{
	if(L == l && R == r)
	{
		++seg[o].tag;
		if(seg[o].tag >= 6)
			seg[o].tag = 2;
		return;
	}
	int M = L + R >> 1;
	push_down(o);
	if(r <= M)
		mul(o + o, L, M, l, r);
	else if(l > M)
		mul(o + o + 1, M + 1, R, l, r);
	else
	{
		mul(o + o, L, M, l, M);
		mul(o + o + 1, M + 1, R, M + 1, r);
	}
	push_up(o);
}
int query(int o, int L, int R, int l, int r)
{
	if(L == l && R == r)
		return seg[o].sum[seg[o].tag];
	int M = L + R >> 1, ret = 0;
	push_down(o);
	if(r <= M)
		ret = query(o + o, L, M, l, r);
	else if(l > M)
		ret = query(o + o + 1, M + 1, R, l, r);
	else
	{
		ret = query(o + o, L, M, l, M) + query(o + o + 1, M + 1, R, M + 1, r);
		if(ret >= mod)
			ret -= mod;
	}
	push_up(o);
	return ret;
}
int main()
{
	while(scanf("%d%d", &n, &m) == 2)
	{
		memset(seg, 0, sizeof seg);
		build(1, 1, n);
		while(m--)
		{
			int l, r;
			char op[2];
			scanf("%s%d%d", op, &l, &r);
			if(op[0] == 'S')
				mul(1, 1, n, l, r);
			else
				printf("%d\n", sqr(query(1, 1, n, l, r)));
		}
	}
	return 0;
}

I. Microsoft每周日麻训练

题意:

给定麻将一个初始牌面,再给定接下来牌池的依次会出现的牌面,保证可以在没牌拿之前胡牌,胡牌形式有三种(普通胡、七对子、国士无双),对于每组数据输出合法的方案。

题解:

如果不考虑比较坑爹的评测机,那么这是一道很简单的模拟题,对所有的牌进行一次判定胡牌的操作,看要选哪些牌,输出即可。

但是这个题有Special Judge,是根据选手的输出序列模拟麻将操作,在现在的OJ上需要尽量让输出序列短才能在比较器不超时的情况下AC,所以还需要二分牌池里要摸的牌数,找出最早胡牌的策略。

代码:

#include <cstdio>
#include <cstring>
const char *out = "mspc";
int n, now[20], seq[150], all[40], wan[40], fin, push[150];
char str[150];
int trans(char *s)
{
	for(int i = 0; i < 4; ++i)
		if(s[1] == out[i])
			return i * 9 + s[0] - '1';
	return -1;
}
bool matchless()
{
	bool flag = 0;
	for(int i = 0; i < 3; ++i)
	{
		if(!all[i * 9] || !all[i * 9 + 8])
			return 0;
		--all[i * 9], --all[i * 9 + 8];
		++wan[i * 9], ++wan[i * 9 + 8];
		if(!flag && all[i * 9])
		{
			--all[i * 9];
			++wan[i * 9];
			flag = 1;
		}
		if(!flag && all[i * 9 + 8])
		{
			--all[i * 9 + 8];
			++wan[i * 9 + 8];
			flag = 1;
		}
	}
	for(int i = 0; i < 7; ++i)
	{
		if(!all[27 + i])
			return 0;
		--all[27 + i];
		++wan[27 + i];
		if(!flag && all[27 + i])
		{
			--all[27 + i];
			++wan[27 + i];
			flag = 1;
		}
	}
	if(flag)
		return 1;
	return 0;
}
bool sevenpair()
{
	int cnt = 0;
	for(int i = 0; i < 34 && cnt < 7; ++i)
		if(all[i] >= 2)
		{
			all[i] -= 2;
			wan[i] += 2;
			++cnt;
		}
	if(cnt == 7)
		return 1;
}
bool check(int dep)
{
	if(dep == 4)
	{
		for(int i = 0; i < 34; ++i)
			if(all[i] >= 2)
			{
				all[i] -= 2;
				wan[i] += 2;
				return 1;
			}
		return 0;
	}
	for(int i = 0; i < 34; ++i)
		if(all[i] >= 3)
		{
			all[i] -= 3;
			wan[i] += 3;
			if(check(dep + 1))
				return 1;
			all[i] += 3;
			wan[i] -= 3;
		}
	for(int t = 0; t < 3; ++t)
		for(int j = 0; j < 7; ++j)
		{
			int i = t * 9 + j;
			if(all[i] && all[i + 1] && all[i + 2])
			{
				--all[i], --all[i + 1], --all[i + 2];
				++wan[i], ++wan[i + 1], ++wan[i + 2];
				if(check(dep + 1))
					return 1;
				++all[i], ++all[i + 1], ++all[i + 2];
				--wan[i], --wan[i + 1], --wan[i + 2];
			}
		}
	return 0;
}
void rebuild()
{
	for(int i = 0; i < 34; ++i)
	{
		all[i] += wan[i];
		wan[i] = 0;
	}
}
bool judge(int lim)
{
	memset(all, 0, sizeof all);
	memset(wan, 0, sizeof wan);
	for(int i = 0; i < 13; ++i)
		++all[now[i]];
	for(int i = 0; i < lim; ++i)
		++all[seq[i]];
	if(!matchless())
	{
		rebuild();
		if(!sevenpair())
		{
			rebuild();
			if(!check(0))
				return 0;
		}
	}
	return 1;
}
int main()
{
	while(scanf("%s", str) != EOF)
	{
		now[0] = trans(str);
		++all[now[0]];
		for(int i = 1; i < 13; ++i)
		{
			scanf("%s", str);
			now[i] = trans(str);
			++all[now[i]];
		}
		scanf("%d", &n);
		for(int i = 0; i < n; ++i)
		{
			scanf("%s", str);
			seq[i] = trans(str);
			++all[seq[i]];
		}
		int L = 0, R = n, M;
		while(L < R)
		{
			M = L + R >> 1;
			if(judge(M))
				R = M;
			else
				L = M + 1;
		}
		judge(L);
		fin = push[0] = 0;
		for(int i = 0; i < 13; ++i)
			if(wan[now[i]])
			{
				--wan[now[i]];
				++fin;
			}
			else
				push[++push[0]] = now[i];
		for(int i = 0; i < n; ++i)
		{
			if(wan[seq[i]])
			{
				--wan[seq[i]];
				++fin;
			}
			else
				push[++push[0]] = seq[i];
			if(fin == 14)
			{
				puts("Ron");
				break;
			}
			printf("%d%c\n", push[push[0]] % 9 + 1, out[push[push[0]] / 9]);
			--push[0];
		}
	}
	return 0;
}

小记

我很好奇我是怎么在精度大战中存活的,感觉自己弱爆了。现场赛的开题顺序不对,当时的做题心态也不是很好,不过终于补全了题目,尤其是最后一题,如果我NOI没写那个麻将AI估计这题是连题目都不会想看的吧。

你可能感兴趣的:(dp,模拟,数学,线段树,贪心)