1. 程式人生 > >資訊學奧賽一本通 小球(drop)

資訊學奧賽一本通 小球(drop)

2018年資訊學奧賽NOIP資料下載
This drop is gonna last forever!

許多的小球一個一個的從一棵滿二叉樹上掉下來組成FBT(Full Binary Tree,滿二叉樹),每一時間,一個正在下降的球第一個訪問的是非葉子節點。然後繼續下降時,或者走右子樹,或者走左子樹,直到訪問到葉子節點。決定球運動方向的是每個節點的布林值。最初,所有的節點都是false,當訪問到一個節點時,如果這個節點是false,則這個球把它變成true,然後從左子樹走,繼續它的旅程。如果節點是true,則球也會改變它為false,而接下來從右子樹走。滿二叉樹的標記方法如下圖:

因為所有的節點最初為false,所以第一個球將會訪問節點1,節點2和節點4,轉變節點的布林值後在在節點8停止。第二個球將會訪問節點1、3、6,在節點12停止。明顯地,第三個球在它停止之前,會訪問節點1、2、5,在節點10停止。

現在你的任務是,給定FBT的深度D,和I,表示第I個小球下落,你可以假定I不超過給定的FBT的葉子數,寫一個程式求小球停止時的葉子序號

——以上是題目;
當第一次看到這道題的時候,好像是11月11日(巧了),剛剛自己學習了建樹的蒟蒻的我馬上想到了去模擬這個過程。原理很簡單,用指標去建樹,然後每個結構體內去維護四個變數:

1、bool check ---- 判斷是否往左還是往右; 2、int data ---- 記錄該結點的值;

3、node *lchild ---- 指向左兒子的指標; 4、node *rchild ---- 指向右兒子的指標;

#include<bits/stdc++.h>
using namespace std;
typedef struct node;
typedef node tree;
struct node{
char data;
bool check;
tree lchild,rchild;
};
tree root;
char c;
int D;
long long num, I, ans, i;
void build_bt(tree &bt,int sum,int num){
if(sum>D){
return;
}
bt=new node;
bt->data=num;
bt->check=false;
build_bt(bt->lchild,sum+1,num

2);
build_bt(bt->rchild,sum+1,num*2+1);
}
int solve(int depth,tree &bt){
if(depth==D)
return bt->data;
if(bt->check){
bt->check=false;
solve(depth+1,bt->rchild);
}
else{
bt->check=true;
solve(depth+1,bt->lchild);
}
}
int main(){
cin>>D>>I;
build_bt(root,1,1);
for(i=1;i<=I;i++){
ans=solve(1,root);
}
cout<<ans;
return 0;
}
只要建了樹,一遍一遍跑就是了,捫心一想:嗯,一定是將會AC了;

信心滿滿提交然而並沒有AC,WA!沒有爆記憶體,也沒有超時,就是WA,GG(撓頭)。

改了很久都還是WA,心中mmp罵不出來,於是嘗試用陣列去做:利用二叉樹的左右兒子編號分別為為 父親編號2和 父親編號2+1,構建一個數組去模擬,還是開bool陣列去判斷掉到左邊還是右邊;

#include<bits/stdc++.h>
using namespace std;
int tr[1<<20];
long long I,D,ans;
long long tree_doit(){
int node=1;
for(int i=1;i<=D-1;i++){
if(!tr[node]){
tr[node]=1;
node=node2;
}
else{
tr[node]=0;
node=node
2+1;
}
}
return node;
}
int main(){
cin>>D>>I;
for(int i=1;i<=I;i++)
ans=tree_doit();
cout<<ans;
return 0;
}
是AC掉了沒錯,但還是覺得不對,一定有規律可循,想啊想,豁然開朗(噗)。

不難發現我們所需要求的是最後一個小球的位置,那我們可不可以只去模擬最後一個小球的掉落呢?

有人會問:你怎麼知道球會怎麼落吶?難道不是需要去求出前 I-1 個球所改變的方向嗎?

但我們根本不需要知道。

首先球在第一層只會落兩個方向:左,右;說明我們球落到球要麼在一邊,要麼在另一邊,所以 I 個球落下會在剩下一個球或0

個時,左右是”均分“的。

由於球是先從左邊開始落的,落下奇數個球一定是落在左邊的,偶數個一定是落在右邊的。可以類比一下走路,假如您邁出的第一步是左腳,那麼走奇數步時正在邁出的一定是左腳,而不是右腳。

(假如題目改一下,小球先從右邊落,在從左落下,道理也是一樣的!)

這只是第一層,第二層,第三層 …… 第D層呢?

我們可以發現,無論是在哪一層,小球都需要用一半([I/2](向下取整))個小球去克服兩邊的每一邊,最後的那個球,和第一層一樣,只是球的總數變成了原來的1/2;

但是不是左右兩邊都會剩下一半呢?不是,左邊會多剩下一個,還是那個道理,左腳先邁出,左腳便有”先決優勢“。

還是利用那個父與子的編號性質,左掉編號2,右掉編號2+1。

好啦,放程式碼:

#include<bits/stdc++.h>
using namespace std;
int ans=1,D,I;
int main(){
cin>>D>>I;
for(int i=2;i<=D;i++)
if(I%2==1){
ans*=2;
I=I/2+1;
}
else{
ans=ans*2+1;
I/=2;
}
cout<<ans;
return 0;
}
嗯,AC,而且程式碼顯然剪短了許多。
但是我還是不明白為什麼建樹不對,好歹程式碼也是這麼長的啊。

順便說一句,程式碼長短好像並不是解決題目的本質,要了解題意,仔細推敲,找準規律,一針見血!(聽說YJQ神牛寫線性篩的某一道題,最後寫了老長,發現根本不用線性篩一樣)

原文:https://blog.csdn.net/Jerry_wang119/article/details/79056076