Ants
题意
有一棵树,从0时刻开始,有两只蚂蚁分别从树的左右两侧出发遍历这棵树。左蚂蚁从一条边的下方爬到上方需要2秒,从上方爬到下方需要1秒,右蚂蚁的爬行速度是左蚂蚁的两倍。蚂蚁如果碰到对方就会立刻调头继续爬行,蚂蚁如果返回地面也会调头继续爬行。计算两只蚂蚁第二次相遇的时间。
输入
t 组数据,每组数据包含一个整数 n 代表边数和一个 n/2 个字符的字符串。这个字符串本质上是一个大十六进制数,如果转化为二进制数就可以表示这棵树的信息,具体表示方法为假设右蚂蚁不动的情况下左蚂蚁的运动轨迹,当当前位置为1时表示蚂蚁向上爬,否则表示蚂蚁向下爬。
1 ≤ t ≤ 1000 , 2 ≤ n ≤ 100 , 000 , 000 1 \le t \le 1000,\ 2 \le n \le 100,000,000 1≤t≤1000, 2≤n≤100,000,000 输入数据量不超过 50 MB。注意空间限制为 6 MB,远远小于输入数据量。
输出
输出一个最简分式 p/q ,表示答案。如果答案为整数,q = 1
样例
1
28
fb1da30d1b7230
282/5
样例解释
转化为二进制即为上图的树。
1111 1011 0001 1101 1010 0011 0000 1101 0001 1011 0111 0010 0011 0000
解法
首先我们观察到,显然,左边的蚂蚁将在第一次相遇后首先回到地面,因为右蚂蚁需要向上爬的比率总是较高。
于是就可以轻松嘴出算法的大概流程:
- 读入树的大小。于是我们就知道了有n条上升的边和n条下降的边。
- 当读入树的信息时,我们就可以计算出左蚂蚁的行动轨迹和需要的时间。由于上升和下降边的个数是已知的,所以右蚂蚁走到当前位置的时间也是可以算出来的。所以当两只蚂蚁第一次相遇的时候,我们就可以知道。
- 当两只蚂蚁第一次相遇后,按照右蚂蚁的行动轨迹继续模拟,直到左蚂蚁回到地面。左蚂蚁回到地面的时间是可以计算出来的,而根据我们先前的观察,此时右蚂蚁还没有回到地面。
- 然后继续模拟右蚂蚁的运动,直到两只蚂蚁再次相遇。
听上去真的非常轻松啊?现在来形式化地描述一下这个流程。
用实数
a
a
a 表示左蚂蚁目前经过了多少条边,用实数
k
k
k 表示左蚂蚁目前的高度,用
t
g
t_g
tg 表示左蚂蚁向上爬需要的时间,
t
d
t_d
td 表示左蚂蚁向下爬需要的时间。于是有到达某个点的总时间的公式:
[
a
,
k
,
t
g
,
t
d
]
=
a
+
k
2
t
g
+
a
−
k
2
t
d
[a,k,t_g,t_d] = \frac{a+k}{2}t_g+\frac{a-k}{2}t_d
[a,k,tg,td]=2a+ktg+2a−ktd
于是就可以列出两只蚂蚁第一次相遇的方程
[
a
1
,
k
1
,
2
,
1
]
=
[
2
n
−
a
1
,
k
,
1
,
1
2
]
[a_1,k_1,2,1]=[2n-a_1,k,1,\frac{1}{2}]
[a1,k1,2,1]=[2n−a1,k,1,21]
如前所述,我们可以在读入的时候判断相遇点是否会发生在当前这条边上。当蚂蚁处在节点
(
a
,
k
)
(a,k)
(a,k) 的时候,令
b
=
1
b=1
b=1 表示这是一条上升边,
b
=
−
1
b=-1
b=−1 表示这是一条下降边,于是上式就满足:
a
1
=
a
+
ε
,
k
1
=
k
+
b
ε
,
ε
=
6
n
−
9
a
−
k
9
+
b
a_1=a+\varepsilon,k_1=k+b\varepsilon,\\ \varepsilon = \frac{6n-9a-k}{9+b}
a1=a+ε,k1=k+bε,ε=9+b6n−9a−k
如果
0
≤
ε
<
1
0 \le \varepsilon < 1
0≤ε<1, 我们就找到了第一个相遇的位置。于是就可以求出相遇的时间:
t
1
=
[
a
1
,
k
1
,
2
,
1
]
=
3
a
1
+
k
1
2
t_1=[a_1,k_1,2,1]=\frac{3a_1+k_1}{2}
t1=[a1,k1,2,1]=23a1+k1
然后,我们让左蚂蚁回头,它将在
[
a
1
,
−
k
1
,
2
,
1
]
=
t
1
−
k
1
[a_1,-k_1,2,1]=t_1-k_1
[a1,−k1,2,1]=t1−k1 秒回到树根。此时,右蚂蚁的位置为:
[
2
n
−
a
1
,
−
k
1
,
1
,
1
2
]
=
t
1
−
k
1
2
[2n-a_1,-k_1,1,\frac{1}{2}]=t_1-\frac{k_1}{2}
[2n−a1,−k1,1,21]=t1−2k1
于是我们可以知道,左蚂蚁的确会比右蚂蚁回到树根。
我们继续模拟右蚂蚁的运动,右蚂蚁从
(
a
′
,
k
′
)
(a',k')
(a′,k′) 开始,到
(
a
2
,
k
2
)
(a_2,k_2)
(a2,k2) 的运动满足:
a
2
=
a
′
+
ε
′
,
k
2
=
k
′
+
b
′
ε
′
.
[
a
1
,
−
k
1
,
2
,
1
]
+
[
2
n
−
a
2
,
k
2
,
2
,
1
]
=
[
a
2
−
a
1
,
k
2
−
k
1
,
1
,
1
2
]
,
ε
′
=
12
n
−
9
(
a
′
−
a
1
)
+
(
k
′
−
k
1
)
9
−
b
′
a_2=a'+\varepsilon', k_2=k'+b'\varepsilon'.\\ [a_1,-k_1,2,1]+[2n-a_2,k_2,2,1]=[a_2-a_1,k_2-k_1,1,\frac{1}{2}],\\ \varepsilon'=\frac{12n-9(a'-a_1)+(k'-k_1)}{9-b'}
a2=a′+ε′,k2=k′+b′ε′.[a1,−k1,2,1]+[2n−a2,k2,2,1]=[a2−a1,k2−k1,1,21],ε′=9−b′12n−9(a′−a1)+(k′−k1)
如果
0
≤
ε
′
≤
1
0\le \varepsilon' \le 1
0≤ε′≤1, 那么我们就找到了第二个相遇点。这个时间就是
t
2
=
[
a
2
−
a
1
,
k
2
−
k
1
,
1
,
1
2
]
=
3
(
a
2
−
a
1
)
+
(
k
2
−
k
1
)
4
t_2=[a_2-a_1,k_2-k_1,1,\frac{1}{2}]=\frac{3(a_2-a_1)+(k_2-k_1)}{4}
t2=[a2−a1,k2−k1,1,21]=43(a2−a1)+(k2−k1)
所以,从0秒开始到第二次相遇的总时间为:
t
1
+
t
2
=
3
(
a
1
+
a
2
)
+
(
k
1
+
k
2
)
4
t_1+t_2=\frac{3(a_1+a_2)+(k_1+k_2)}{4}
t1+t2=43(a1+a2)+(k1+k2)
注意到
ε
\varepsilon
ε 总是
1
9
+
b
′
\frac{1}{9+b'}
9+b′1 的倍数。于是
ε
′
\varepsilon'
ε′ 是
9
−
b
(
9
+
b
)
(
9
−
b
′
)
\frac{9-b}{(9+b)(9-b')}
(9+b)(9−b′)9−b 。所以
a
1
,
a
2
,
k
1
,
k
2
a_1,a_2,k_1,k_2
a1,a2,k1,k2 都是
1
800
\frac{1}{800}
8001 的倍数。于是,所有数字都可以用实际数值的 800 倍在 64 位整数的限制下表示。
算法的时间复杂度为 O ( n ) O(n) O(n) ,空间复杂度为 O ( 1 ) O(1) O(1) 。
代码
#include <iostream>
#include <algorithm>
#include <string>
using ll = long long;
ll T, n, x;
ll hextooct(char a) {
if (a <= '9' && a >= '0') {
return ll(a-'0');
}
return 10+ll(a-'a');
}
ll gcd(ll a, ll b) {
ll t;
while(b != 0) {
t = a%b;
a = b;
b = t;
}
return a;
}
int main(int argc, char *argv[]) {
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cin >> T;
while (T--) {
std::cin >> n;
ll nn = n * 800;
ll k = 0, a = 0, eps, eps2, a1, k1, a2, k2;
ll t1 = -1, t2 = -1, t;
std::cin.get();
for (int i = 0; i < n/2; i++) {
x = hextooct(std::cin.get());
for (int j = 0; j < 4; j++) {
ll b = ((x >>(3-j) & 1ll)?1:(-1));
if (t1 < 0) {
eps = (6*nn-9*a-k)/(9+b);
if (eps < 800 && eps >= 0) {
a1 = a+eps, k1 = k+eps*b;
t1 = (3*a1+k1)/(2);
}
} else if(t2 < 0) {
eps2 = (12*nn-9*(a-a1)+(k-k1))/(9-b);
if (eps2 < 800 && eps2 >= 0) {
a2 = a+eps2, k2 = k+eps2*b;
t2 = (3*(a2-a1)+(k2-k1))/(4);
}
}
k += 800 * b;
a += 800;
}
}
t = t1 + t2;
ll divisor = gcd(t, 800);
std::cout << t/divisor << '/' << 800/divisor << '\n';
}
}
吐槽
打算开个新栏目整点有趣的算法题!
感觉自己还是需要写一点算法题获得一点虚假的成就感… 但是老年选手 codeforces 手速跟不上,project euler 又做不动,leetcode 又觉得无聊… 有点… 尴尬啊
然后最近找到了清晰版本的 Looking for a challenge? 的拍照pdf,打算做一做… 因为这个题解质量和题目质量好像很高… 而且基本都是思维题(真的吗),适合我这种数据结构废物
其实 AGC 也不错,但是 AGC 太多了,做不动…
以及图书馆的荐购功能是真的牛逼…
我现在在 szkopul 上的题均 dirt 数肯定超过 20 了…
代码细节还挺多的…
不会一个字符一个字符读入错了一万遍
不会位运算又错了一万遍(读入16进制数之后我竟然从后往前用2模它 真的老了
这个 k 和 a 应该在算 eps 之后更新,又错了一发
最后一共交了 14 发才过…
这个东西在 cf 叫 2014-2015 CT S02E07: Codeforces Trainings Season 2 Episode 7 ,找了一年
后来牛逼网友教了我怎么在 gym 按题目名字搜题 然后我发现我笔记里其实有这个链接(
我搜 amppz 题解,搜到一篇英文博客,我一看怎么这么眼熟,结果是我今年一月自己写的,可能是什么垃圾站自动翻译成英文了…
写这种东西比写 Leetcode 快乐多了…