您的位置:首页 > 其它

【解题报告】2015ACM/ICPC亚洲区上海站

2016-02-01 22:22 295 查看
题目链接

A.An Easy Physics Problem(HDU 5572)

思路

我们可以将问题分为以下 22 种情况:

球与圆柱相撞

球与圆柱不相撞

我们可以通过判断以 AA 为起点以 V⃗ \vec V 为方向的射线与圆(题目中的圆柱的俯视)的交点的个数来判断输入是那种情况。针对这 22 种情况,我们可以得到以下 22 种判断方法:

当情况 11 发生时,设求与圆的第 11 个交点为 II 。当 BB 在线段 AIAI 上时球经过 BB 。或者,球反弹后方向合适,球也会经过 BB 。那么怎么知道反向合不合适呢?我们可以做 AA 关于直线 OIOI ( OO 是圆(题目中的圆柱的俯视)圆心)的对称点 A′A' 判断 A′A' 是否在线段 IBIB 上,若 A′A' 在 IBIB 上则球反弹后的方向合适。

当情况2发生时,设线段AB的方向向量为 v⃗ \vec v ,当 v⃗ \vec v 对应的单位向量等于 V⃗ \vec V (球的初速度的方向向量)对应的单位向量时,球的方向合适,于是球会经过 BB 。

代码

#include <bits/stdc++.h>
using namespace std;

const double eps = 1e-8;

// 几何误差修正
inline int cmp(double x) {
return x < -eps ? -1 : (x > eps);
}

// 计算x的平方
inline double sqr(double x) {
return x * x;
}

// 开方误差修正
inline double mySqrt(double n) {
return sqrt(max((double)0, n));
}

// 二维点(向量)类
struct Point {
double x, y;
Point() {}
Point(double x, double y): x(x), y(y) {}
void input() {
scanf("%lf%lf", &x, &y);
}
friend Point operator + (const Point& a, const Point& b) {
return Point(a.x + b.x, a.y + b.y);
}
friend Point operator - (const Point& a, const Point& b) {
return Point(a.x - b.x, a.y - b.y);
}
friend bool operator == (const Point& a, const Point& b) {
return cmp(a.x - b.x) == 0 && cmp(a.y - b.y) == 0;
}
friend Point operator * (const Point& a, const double& b) {
return Point(a.x * b, a.y * b);
}
friend Point operator * (const double& a, const Point& b) {
return Point(a * b.x, a * b.y);
}
friend Point operator / (const Point& a, const double& b) {
return Point(a.x / b, a.y / b);
}
// 返回本向量的长度
double norm() {
return sqrt(sqr(x) + sqr(y));
}
// 返回本向量对应的单位向量
Point unit() {
return Point(x, y) / norm();
}
};

// 求向量间叉积
double det(const Point& a, const Point& b) {
return a.x * b.y - a.y * b.x;
}

// 求向量间点积
double dot(const Point &a, const Point& b) {
return a.x * b.x + a.y * b.y;
}

// 求点到直线的投影
Point getLineProjection(Point P, Point A, Point B) {
Point v = B - A;
return A + v * dot(v, P - A) / dot(v, v);
}

// 求点关于直线的对称点
Point mirrorPoint(Point p, Point s, Point t) {
Point i = getLineProjection(p, s, t);
return 2 * i - p;
}

// 判断点是否在直线上
bool pointOnSegment(Point p, Point s, Point t) {
return cmp(det(p - s, t - s)) == 0 && cmp(dot(p - s, p - t)) <= 0;
}

// 求直线(线段、射线)与圆相交的交点
void circleCrossLine(Point a, Point b, Point o, double r, Point ret[], int& num) {
double x0 = o.x, y0 = o.y;
double x1 = a.x, y1 = a.y;
double x2 = b.x, y2 = b.y;
double dx = x2 - x1, dy = y2 - y1;
double A = sqr(dx) + sqr(dy);
double B = 2 * dx * (x1 - x0) + 2 * dy * (y1 - y0);
double C = sqr(x1 - x0) + sqr(y1 - y0) - sqr(r);
double delta = sqr(B) - 4 * A * C;
num = 0;
if(cmp(delta) < 0) {
return;
}
double t1 = (-B - mySqrt(delta)) / (2 * A);
double t2 = (-B + mySqrt(delta)) / (2 * A);
// 交点只能在射线上
if(cmp(t1) >= 0) {
ret[num++] = Point(x1 + t1 * dx, y1 + t1 * dy);
}
// 交点只能在射线上
if(cmp(t2) >= 0) {
ret[num++] = Point(x1 + t2 * dx, y1 + t2 * dy);
}
}

int t;
double r;
Point O, A, V, B, ins[2];

// 判断球是否会经过B
bool ok() {
// num保存交点个数
int num;
// ins数组保存交点
circleCrossLine(A, A + V, O, r, ins, num);
// 相切或相离
if(num < 2) {
Point v = B - A;
if(v.unit() == V.unit()) {
return true;
}
}
// 相交
else {
// 第一个交点为i
Point i = ins[0];
// 判断B是否在Ai上
if(pointOnSegment(B, A, i)) {
return true;
}
// 求A关于Oi的对称点
Point m = mirrorPoint(A, O, i);
if(m == A && pointOnSegment(A, i, B)) {
return true;
}
// 判断m是否在iB上
if(pointOnSegment(m, i, B)) {
return true;
}
}
return false;
}

int main() {
scanf("%d", &t);
for(int c = 1; c <= t; c++) {
printf("Case #%d: ", c);
O.input();
scanf("%lf", &r);
A.input();
V.input();
B.input();
puts(ok() ? "Yes" : "No");
}
return 0;
}


B. Binary Tree(HDU5573)

思路

本题是要我们通过走K层完全二叉树的方式构造一个整数N。“完全二叉树”和“构造整数”让我们可以联想到“任意整数都有其二进制表示”,从而联想到“任意整数都可以由 1,2,4,...,2n1, 2, 4, ..., 2^n 这样的数相加组成”。

传统的构造方式应该是通过在以1为首项2为公比的等比数列中选择一些数,使其相加,就能得到需要构造的数(例如 11=1+2+811 = 1 + 2 + 8 )。观察完全二叉树最左边的一条链1-2-4-8-…,由于N≤2KN \leq 2^K ,所以沿着这条链最多走包括根节点的K个结点(选出一些结点来)就能构造出 2K−12^{K-1} 以内的数, 例如当 K=3K = 3 时,走沿着最左边的链走三个结点能够构造出7以内的数。这也就说明了这个结论:在传统的构造方式中,沿着完全二叉树的最左边的链走K - 1个结点来到v结点,再走到v的左儿子或右儿子上,将沿途的一些货全部结点挑出并令他们相加,就能构造出N以内的所有整数。例如当 K=3,N=8K = 3, N = 8 时,沿着最左边的链走两个结点来到值为2的结点上,然后走右儿子来到值为5的结点上,把1,3和5相加就能得到8了。

虽然如此,但是本题要求的构造方式与传统的构造方式有所不同。沿途经过的结点的值不是选还是不选的问题,而是加还是减的问题。”减”相比“选”而言,最后的和减少了一个偶数(两倍结点权值)。因此我们计算出 d=2K−Nd = 2^K - N,当d为偶数时最后一个结点选择右儿子,当d为奇数是最后一个结点选择左儿子,然后计算出 d=d/2d = d / 2 ,这个d就是为了让构造结果为N,我们需要减掉的权值之和。最后不难找出哪些结点的权值是需要减去的。

代码

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
int t, n, k;

int main() {
cin >> t;
for(int kase = 1; kase <= t; kase++) {
printf("Case #%d:\n", kase);
cin >> n >> k;
// 计算2^K - N的差值
ll d = ((ll)1 << k) - n;
for(int i = 1; i < k; i++) {
// 输出结点对应权值
cout << ((ll)1 << i - 1) << ' ';
bool flag = ((ll)1 << i) & d;
// 输出权值对应符号
cout << (flag ? '-' : '+') << endl;
}
// 表示差值的奇偶性
bool odd = d & 1;
ll ans = (ll)1 << k - 1;
// 奇偶性决定最后一个结点的选择
cout << (odd ? ans : ans + 1) << " +" << endl;
}
return 0;
}


D.Discover Water Tank(HDU 5575)

思路

首先,直觉告诉我们当水箱中没有水时, z=0z = 0 的询问(以下简称为X,而 z=1z = 1 的询问简称为O)的个数就是答案。但是当水箱中有水时,可能还会得到更优的答案。因此我们可以以X的个数为初始答案,通过不断改变水箱的水位来更新答案 ansans 的值。那么应该以什么样的顺序来改变水位呢?显然应该由低到高改变水位,因此我们将O存储到一个数组(这里用的是向量)中并排序,以备后面按顺序枚举。现在假设我们枚举到第i个O,我们需要知道水位向左能溢出到多远向右能溢出到多远。这通过两个循环就能够判断。接下来我们要对当前的O溢出到的每个小水箱x执行以下操作:询问在当前水位高度 hh 下有多少X,有多少O。O的数量减X的数量的差值 dd 就可以用来更新 ansans 了: ans+=dans += d 。思路大体上是这样,实现起来却会遇上很多问题。首先是每次枚举O的时候向左向右溢出都有 O(n)O(n) 的复杂度。当水向左向右溢出以后,实际上O溢出到的小水箱能够合并成一个大水箱这样 O(m)O(m) 次枚举O的复杂度就是 O(n)O(n) 而不是 O(nm)O(nm) 了。要高效地查询和合并,我们可以用并查集来维护每个由小水箱合并而成的大水箱。具体地,合并的时候不仅要合并当前水位之下的O和X的数量,还要维护大水箱的左右两边是哪些水箱以及是哪些挡板。O的数量可以看当前枚举了多少O,X的数量可以用某个能够维护顺序的数据结构来维护,这个数据结构维护结构体 (x,y)(x, y) ,结构体表示的这个X在坐标为 (x,y)(x, y) 的位置。那么我们在计算 xx 小水箱的水位 hh 下的X的时候,就可以在删除数据结构最小元素的同时计数,直到 y≥hy \geq h 即最低位置的X在水位之上了为止。维护顺序的数据结构可以用堆或者平衡树,但是又需要数据能够快速地合并,因此使用由左偏树实现的可并堆是最合适不过了。

总而言之就是以z = 0的询问总数为初始答案,通过枚举z = 1的询问来更新答案,用并查集维护水箱并处理水箱的合并。用由左偏树实现的可并堆来处理水箱的合并及计算单次枚举的答案(涉及查询最小值及删除最小值)。

最后的本题总的复杂度应该是O(n + mlog(m))。截止本题的解题报告写成时HDU上本题用时排名前五的算法都是由下面的代码提交达成的。(Vjudge的五虎将账号)

代码

#include <bits/stdc++.h>
using namespace std;

typedef pair <int, int> query;
const int maxn = 1e5 + 10, maxm = 2e5 + 5;
int t, n, m, x, y, z, ans;
int L[maxn], R[maxn], LH[maxn], RH[maxn], O[maxn], X[maxn];
vector <query> vec;

// 左偏树相关
int tot, v[maxm], l[maxm], r[maxm], d[maxm], Heap[maxn];

// 合并左偏树
int merge(int x, int y) {
if(x == 0) {
return y;
}
if(y == 0) {
return x;
}
if(v[x] > v[y]) {
swap(x, y);
}
r[x] = merge(r[x], y);
if(d[l[x]] < d[r[x]]) {
swap(l[x], r[x]);
}
d[x] = d[r[x]] + 1;
return x;
}

// 初始化可并堆结点
inline int init(int x) {
v[++tot] = x;
l[tot] = r[tot] = d[tot] = 0;
return tot;
}

// 左偏树的插入操作
inline int insert(int x, int y) {
return merge(x, init(y));
}

// 取得左偏树中的最小值
inline int top(int x) {
return v[x];
}

// 弹出左偏树
inline int pop(int x) {
return merge(l[x], r[x]);
}

// 判断左偏树是否非空
inline bool empty(int x) {
return x == 0;
}

//  初始化可并堆
void initHeap() {
memset(Heap, 0, sizeof(Heap));
tot = 0;
}

// 并查集相关
int p[maxn];

// 初始化并查集
void initSet() {
for(int i = 1; i <= n; i++) {
p[i] = i;
}
}

// 查找集合的祖先
int find(int x) {
return x == p[x] ? x : p[x] = find(p[x]);
}

// 合并集合
inline void Union(int x, int y) {
x = find(x);
y = find(y);
if(x == y) {
return;
}
p[y] = x;
if(x < y) {
RH[x] = RH[y];
L[R[x]] = x;
R[x] = R[y];
}
else {
LH[x] = LH[y];
R[L[x]] = x;
L[x] = L[y];
}
// 合并可并堆
Heap[x] = merge(Heap[x], Heap[y]);
X[x] += X[y];
O[x] += O[y];
}

int main() {
scanf("%d", &t);
for(int c = 1; c <= t; c++) {
scanf("%d%d", &n, &m);
LH[1] = RH
= INT_MAX;
L
= n - 1;
for(int i = 1; i < n; i++) {
scanf("%d", &RH[i]);
// 用于快速查询水箱的左右挡板
LH[i+1] = RH[i];
// 用于快速查询左右方水箱
L[i] = i - 1;
R[i] = i + 1;
}
initHeap();
vec.clear();
ans = 0;
while(m--) {
scanf("%d%d%d", &x, &y, &z);
if(z == 1) {
vec.push_back(query(y + 1, x));
}
else {
Heap[x] = Heap[x] ? insert(Heap[x], y) : init(y);
ans++;
}
}
initSet();
sort(vec.begin(), vec.end());
for(int i = 1; i <= n; i++) {
O[i] = X[i] = 0;
}
for(int i = 0; i < vec.size(); i++) {
x = find(vec[i].second);
y = vec[i].first;
// 向左溢出
while(y > LH[x]) {
Union(x, L[x]);
x = find(x);
}
// 向右溢出
while(y > RH[x]) {
Union(x, R[x]);
x = find(x);
}
// 删除水位以下的X
while(!empty(Heap[x]) && top(Heap[x]) < y) {
Heap[x] = pop(Heap[x]);
X[x]++;
}
// 更新答案
if(++O[x] >= X[x]) {
ans += (O[x] - X[x]);
O[x] = X[x] = 0;
}
}
printf("Case #%d: %d\n", c, ans);
}
return 0;
}


F. Friendship of Frog(HDU5578)

思路

先扫描一遍字符串,把国籍相同的青蛙的编号储存在该国籍对应的数组中,然后扫描这些数组,更新最小距离。

代码

#include <bits/stdc++.h>
using namespace std;

const int maxc = 30, maxn = 1010;
int t, len, ans;
char s[maxn];
vector <int> G[maxc];

int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
for(int i = 0; i < 26; i++) {
G[i].clear();
}
scanf("%s", s);
len = strlen(s);
for(int i = 0; i < len; i++) {
G[s[i]-'a'].push_back(i);
}
ans = -1;
for(int i = 0; i < 26; i++) {
if(G[i].size() > 1) {
ans = INT_MAX;
}
}
for(int i = 0; i < 26; i++) {
for(int j = 1; j < G[i].size(); j++) {
ans = min(ans, G[i][j] - G[i][j-1]);
}
}
printf("Case #%d: %d\n", kase, ans);
}
return 0;
}


K. Kingdom of Black and White(HDU5583)

思路

由于输入给的信息不是很适合处理,因此我们把诸如000011的01串处理成数字序列42,每个数字成为一个块。块4代表有4个连续的0,而块2代表有2个连续的1。然后就要求最大的力量值了。一开始我的想法复杂而片面,想用各种特判来解决问题,因为当两个块连在一起的时候,可以用数学方法证明将两个块的交界处的属于更少青蛙数量的块的01值改变能够得到更大的总力量值。于是代码写得一塌糊涂最后导致总是有莫名其妙的bug。

后来发现在复杂度允许的情况下可以用更暴力的方法来解决,那就是枚举每一个块,计算这个块的两头变色以后的新能量并更新。块的青蛙数量为1的时候要特判一下,因为这个青蛙变色会导致该块的左边相邻块和有边相邻块结合到一起。

实现上可以预处理出枚举前的各个块的力量值的平方和sum,和各个块的力量值的平方squ[],要注意结果可能导致溢出(因为有大量的平方,求和运算),另外要在开头和结尾加入两个青蛙数量为0的块,不然会遗漏开头和结尾的块的枚举。

代码

#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ll;
const int maxn = 1e5 + 5;
int t, cnt;
ll sum, ans, squ[maxn];
string s;
vector <ll> vec;

int main() {
cin >> t;
for(int kase = 1; kase <= t; kase++) {
cin >> s;
cnt = 0;
// vec按顺序存放各个块中的青蛙的数量
vec.clear();
// flag表示上一个枚举到的青蛙的颜色
int flag = -1;
// 枚举青蛙来构造块
for(int i = 0; i < s.size(); i++) {
int cur = s[i] - '0';
// 第一次会构造出一个没有青蛙的块
if(cur != flag) {
vec.push_back(cnt);
flag = cur;
cnt = 0;
}
cnt++;
}
// 构造最后一个块
vec.push_back(cnt);
// 构造一个没有青蛙的块
vec.push_back(0);
sum = 0;
// 预处理
for(int i = 0; i < vec.size(); i++) {
sum += vec[i] * vec[i];
squ[i] = vec[i] * vec[i];
}
ans = sum;
// 扫描块,更新总力量值的最大值
for(int i = 1; i < vec.size() - 1; i++) {
// 特判
if(vec[i] == 1) {
int add = vec[i-1] + vec[i+1] + 1;
ans = max(ans, sum - squ[i-1] - squ[i+1] - 1 + add * add);
}
else {
// 左端青蛙变色
int add1 = vec[i-1] + 1, add2 = vec[i] - 1;
ans = max(ans, sum - squ[i-1] - squ[i] + add1 * add1 + add2 * add2);
// 右端青蛙变色
add1 = vec[i+1] + 1, add2 = vec[i] - 1;
ans = max(ans, sum - squ[i+1] - squ[i] + add1 * add1 + add2 * add2);
}
}
printf("Case #%d: ", kase);
cout << ans << endl;
}
return 0;
}


L. LCM Walk(HDU5584)

思路

以Sample Input中的 (6,8)(6, 8) 为例,想求出它的的起点有多少个,就要先求出 (6,8)(6, 8) 的前一个点(相邻的起点)。

假设 (6,8)(6, 8) 的前一个点的坐标为 (x,y)(x, y) ,因为 (6,8)(6, 8) 是由 xx 和 yy 中的一个加上 lcm(x,y)lcm(x, y) ,而另一个保持不变构成的,所以 xx 一定是6。现在我们将问题转化成了求 yy 是多少。根据题意

y+lcm(6,y)=8,(1≤y≤2)y + lcm(6, y) = 8, (1 \leq y \leq 2)

通过枚举 yy 带入检验可得 y=2 y = 2

在理清楚思路以后,我们可以来解决更一般的情况了。假设 (a,b)(a, b) (不妨令 a≤ba \leq b )的前一个点的坐标为 (x,y)(x, y) ,根据题意

y+lcm(a,y)=b,(1≤y≤b−a)y + lcm(a, y) = b, (1 \leq y \leq b - a)

在该范围内枚举 yy 带入检验即可

但是有 1≤a,b≤1091 \leq a, b \leq 10^9 ,因此这样枚举肯定会超时。所以要再对上面的等式变形。

根据数论知识 gcd(a,y)×lcm(a,y)=a×ygcd(a, y) \times lcm(a, y) = a \times y ,

所以 1+agcd(a,y)=by1 + \frac {a} {gcd(a, y)} = \frac {b} {y}

枚举 yy 的时候只要枚举能够整除 bb 的 yy 即可。

这样,每向前走一步都枚举出 y y ,直到 y y 无解就到达了路径的起点,目标点可能的起点数就是路径上经过的点的数量。

代码:

#include <bits/stdc++.h>
using namespace std;

int t, x, y, ans, upperBound, res, tmp;
vector <int> v;

int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}

// 返回能整除n的所有数组成的vector
inline vector <int> factor(int n) {
vector <int> v;
for(int i = 1; i * i <= n; i++) {
if(n % i == 0) {
if(i <= upperBound) {
v.push_back(i);
}
if((n / i) <= upperBound) {
v.push_back(n / i);
}
}
}
return v;
}

int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%d%d", &x, &y);
for(ans = 1;; ans++) {
if(x > y) swap(x, y);
// 枚举的上限
upperBound = y - x;
v = primeFactor(y);
res = -1;
// 枚举v中的元素
for(int i = 0; i < v.size(); i++) {
tmp = (y / v[i] - 1) * gcd(v[i], x);
// 检验是否满足等式
if(tmp == x) {
res = v[i];
break;
}
}
if(res == -1) break;
y = res;
}
printf("Case #%d: %d\n", kase, ans);
}
return 0;
}


(其它题目略)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: