题解:P1797 【模板】Stern-Brocot 树
这一道题其实实现难度并不高,主要考验数学知识。只要我们了解了 Stern–Brocot 树的性质,AC 这道题也就很容易了。
1. Stern–Brocot 树是什么
这是解出这一题的必备知识。Stern–Brocot 树是一种维护分数的优雅的结构,包含所有不同的正有理数。并且与 连分数 紧密相关,在算法竞赛中可以用于解决一系列的数论问题。
而这棵树按以下的方法构造:
Stern–Borcot 树可以在迭代构造第
在第
将每次迭代中新添加的分数连结成树状结构,就得到了 Stern–Brocot 树。听上去有点懵,一图胜千言,下图就是前几层 Stern–Brocot 树的样子:
(图片来自oi-wiki)
Stern–Brocot 树还有几条性质:
- 最简性:Stern–Brocot 树上的所有分数都为既约分数。
- 单调性:Stern–Brocot 树的中序遍历单调递增。
- 完整性:Stern–Brocot 树不重不漏的包含了所有既约分数。
具体的数学证明可以在 oi wiki 上找到,接下来我重点讲一下几种询问的编程思路。
2. ENCODE_PATH 实现
首先是 ENCODE_PATH,也就是给定分数
则设首组向右移动的次数为零。向右、向左交替移动端点,将每组移动后的端点位置排列如下:
其中,偶数组移动是向右的,故而记录的是左端点的位置;奇数组移动是向左的,故而记录的是右端点的位置。在这一列端点前面再添加两个端点
如果连分数展开式
而最后得到的连分数就是
因此,在目标分数的末尾为一的连分数表示中,不计最后的一,前面的项就编码了 Stern–Brocot 树上自根节点到当前节点的路径。其中,偶数项(下标自
代码如下:
void en(int x,int y){
vector<pair<int, char>> res;
bool right = true;//先向右走
while(y){
res.emplace_back(x / y, right ? 'R' : 'L');
x %= y;
swap(x, y);
right ^= 1;//改变方向
}//表达成连分数的形式
res.back().first--;//连分数的最后一项减一
if(res.front().first){
printf("%d ",res.size());
for(auto i=res.begin();i!=res.end();i++){
printf("%c %d ",i->second,i->first);
}
return;
}//res首项如果为{0,'R'}不输出。
printf("%d ",res.size()-1);
for(auto i=res.begin()+1;i!=res.end();i++){
printf("%c %d ",i->second,i->first);
}
}
3. ENCODE_PATH 实现
这一个询问就是反过来,知道路径输出分数。道理也是一样的,先把路径转换成连分数的形式,再把连分数转换成分数。但要注意的是,如果给出的路径是先往左走,那就得让首项为
参照上文提到的连分数地推公式进行编写,核心代码如下:
void de(vector<int> a) {//a是连分数对应的数组,在此之前还要对路径进行上文的处理
vector<int> p={0,1};
vector<int> q={1,0};
for (auto it:a) {
p.push_back(p.back()*it+p.end()[-2]);
q.push_back(q.back()*it+q.end()[-2]);
}
printf("%d %d ",p.back(),q.back());
}
这段代码在之后的询问中也会用到。
4. LCA 实现
同样根据上文将的原理,这其实就是求两个分数对应的两条路径的最长公共前缀对应的分数。思路就是按上文的方法先得到两个分数的路径,然后从前往后比较。例如样例中的
void lca(int a,int b,int c,int d){
vector<pair<int,char>> res1,res2;//两个分数对应的连分数
bool right=1,flg=0;
while(b){
res1.emplace_back(a/b, right ? 'R' : 'L');
a%=b;
swap(a,b);
right^=1;
}
res1.back().first--;
right=1;
while(d){
res2.emplace_back(c/d,right?'R':'L');
c%=d;
swap(c,d);
right^=1;
}
res2.back().first--;
vector<int> r;//lca对应的连分数
for(int i=0;i<min(res1.size(),res2.size());i++){
if(res1[i].second!=res2[i].second)break;//方向不同停止
else{
r.push_back(min(res1[i].first,res2[i].first));
if(res1[i].first!=res2[i].first)break;//移动步数不同也停止
}
}
r.push_back(1);
de(r);//把lca连分数转换成分数
}
5. ANCESTOR 实现
依然是用连分数的原理,在把分数转换成连分数路径时到 -1。
举个例子,
代码如下:
void anc(int k,int a,int b){
string res;
if(a+b>2){
bool right=1,flg=0;
while(b){
for(int i=0;i<a/b;i++){
res+=right ? 'R' : 'L';
if(res.size()>k){
flg=1;
break;
}//深度超过k了就退出
}
a%=b;
swap(a,b);
right^=1;
if(flg)break;
}
res.pop_back();
}
if(res.size()<k){
cout<<-1;return;
}
string s=res.substr(0,k);//取前k位路径
vector<int>r={0};
char x='R';
for(int i=0;i<s.size();i++){
if(s[i]==x)r.back()++;
else {
r.push_back(1);
x=s[i];
}
}
r.push_back(1);//转换成de函数可以处理的形式
de(r);
}
6. RANGE 实现
这个询问其实就是在问这个分数在构建树的时候,
因此,可以直接通过
代码如下:
void ran(int x,int y){
if(x==1&&y==0){
cout<<"1 0";
return;
}else if(x==0&&y==1){
cout<<"0 1";
return;
}//两个特判
int a=0,b=1,c=1,d=0;
bool right = true;
while(x!=a+c||y!=b+d){//寻找两个分数
if(right){//决定移动方向
auto t = (b*x-a*y-1)/(y*c-x*d);
a+=t*c;
b+=t*d;//缩小范围,下同
}else{
auto t=(y*c-x*d-1)/(b*x-a*y);
c+=t*a;
d+=t*b;
}
right^=1;
}
printf("%d %d %d %d",a,b,c,d);
}
7. 完整程序
最后把所有询问功能组合起来再加上输入、输出和一部分处理就是完整程序了,注释可以看前面。
码风偏差,请多多见谅。
#include<bits/stdc++.h>
using namespace std;
void en(int x,int y){
vector<pair<int,char>> res;
bool right = true;
while(y){
res.emplace_back(x/y,right?'R':'L');
x%=y;
swap(x,y);
right^=1;
}
res.back().first--;
if(res.front().first){
printf("%d ",res.size());
for(auto i=res.begin();i!=res.end();i++)printf("%c %d ",i->second,i->first);
return;
}
printf("%d ",res.size()-1);
for(auto i=res.begin()+1;i!=res.end();i++){
printf("%c %d ",i->second,i->first);
}
}
void de(vector<int> a) {
vector<int> p={0,1},q={1,0};
for (auto it:a) {
p.push_back(p.back()*it+p.end()[-2]);
q.push_back(q.back()*it+q.end()[-2]);
}
printf("%d %d ",p.back(),q.back());
}
void lca(int a,int b,int c,int d){
vector<pair<int,char>> res1,res2;
bool right=1,flg=0;
while(b){
res1.emplace_back(a/b, right ? 'R' : 'L');
a%=b;
swap(a,b);
right^=1;
}
res1.back().first--;
right=1;
while(d){
res2.emplace_back(c/d,right?'R':'L');
c%=d;
swap(c,d);
right^=1;
}
res2.back().first--;
vector<int> r;
for(int i=0;i<min(res1.size(),res2.size());i++){
pair<int,char> m=res1[i],n=res2[i];
if(m.second!=n.second)break;
else{
r.push_back(min(m.first,n.first));
if(m.first!=n.first)break;
}
}
r.push_back(1);
de(r);
}
void anc(int k,int a,int b){
string res;
if(a+b>2){
bool right=1,flg=0;
while(b){
for(int i=0;i<a/b;i++){
res+=right?'R':'L';
if(res.size()>k){
flg=1;
break;
}
}
a%=b;
swap(a,b);
right^=1;
if(flg)break;
}
res.pop_back();
}
if(res.size()<k){
printf("-1");return;
}
string s=res.substr(0,k);
vector<int>r={0};
char x='R';
for(int i=0;i<s.size();i++){
if(s[i]==x)r.back()++;
else {
r.push_back(1);
x=s[i];
}
}
r.push_back(1);
de(r);
}
void ran(int x,int y){
if(x==1&&y==0){
printf("1 0");
return;
}else if(x==0&&y==1){
printf("0 1");
return;
}
int a=0,b=1,c=1,d=0;
bool right = true;
while(x!=a+c||y!=b+d){
if(right){
auto t = (b*x-a*y-1)/(y*c-x*d);
a+=t*c;
b+=t*d;
}else{
auto t=(y*c-x*d-1)/(b*x-a*y);
c+=t*a;
d+=t*b;
}
right^=1;
}
printf("%d %d %d %d",a,b,c,d);
}
int main(){
int T;
scanf("%d",&T);
while(T--){
char s[1000],tmp;int x,y;
string k;
cin>>k>>x;
if(k=="DECODE_PATH"){
vector<int> r;
bool first=1;
while(x--){
cin>>tmp>>y;
if(first && tmp=='L'){r.push_back(0);}
first=0;
r.emplace_back(y);
}
r.push_back(1);//路径转连分数
de(r);
}else if(k=="ENCODE_PATH"){
scanf("%d",&y);
en(x,y);
}else if(k=="LCA"){
int z,a;
scanf("%d%d%d",&y,&z,&a);
lca(x,y,z,a);
}else if(k=="ANCESTOR"){
int z;
scanf("%d%d",&y,&z);
anc(x,y,z);
}else{
scanf("%d",&y);
ran(x,y);
}
putchar('\n');
}
return 0;
}
注:部分代码借鉴自 oi-wiki。