比赛地址
第十届北航程序设计竞赛现场决赛
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排序,从新序列里取中位数作为中心直接计算代价即可,时间复杂度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;
sort(a, a + n);
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估计这题是连题目都不会想看的吧。