1. 程式人生 > 實用技巧 >模擬畫圖題P1185 繪製二叉樹

模擬畫圖題P1185 繪製二叉樹

  題目連結P1185 繪製二叉樹

題意概述

  根據規則繪製一棵被刪去部分節點的滿二叉樹。節點用 \(o\) 表示,樹枝用/\表示。每一層樹枝長度會變化,以滿足葉子結點有如下特定:

  • 相鄰葉子節點是兄弟節點(同一個父親)時,間隔 \(3\) 個空格。
  • 相鄰葉子節點不是兄弟節點,之間隔一個空格。

  一棵層數為 \(4\) 的滿二叉樹長這樣(可能會出現因為字元寬度不一而出現偏移):

           o           
          / \          
         /   \         
        /     \        
       /       \       
      /         \      
     o           o     
    / \         / \    
   /   \       /   \   
  o     o     o     o  
 / \   / \   / \   / \ 
o   o o   o o   o o   o

  刪除節點的輸入格式為:刪除第 \(i\) 層從左往右數的第 \(j\) 個節點。注意刪除時,把原有的字元用空格替換,結果是要列印空格的

分析

  又是一道畫圖模擬題,需要耐心分析。我採取的是找規律的方法,程式碼可能長,但是應該比較容易理解吧 \(QAQ\)
  先看我們得維護什麼資訊才能實現初始化滿二叉樹和刪點兩個操作。初始化的方法有挺多,可以先鋪好葉子結點,往上遞迴建樹,也可以從根節點往下建樹。但是有個問題是我們並不知道葉子結點到根節點的垂直距離,也不知道根節點的座標。這時候我們就得找樹枝的規律了。建好樹後我們要刪點,但是輸入點的方式不能直接確定點的座標,得找同一層節點分佈的規律。為了後續討論方便,我們約定葉子節點為第一層,根節點在第 \(m\)

樹枝的規律

  打表加看圖硬分析(這裡的樹枝長定義為連線第 \(i\) 層節點與第 \(i+1\) 層節點的樹枝長,表格有些許錯位):

層數 \(1\) \(2\) \(3\) \(4\) \(5\)
樹枝長 \(len\) \(1\) \(2\) \(5\) \(11\) \(23\)
規律 \(1\) \(1+(2-1)\) \((1+2)+(3-1)\) \((1+2+5)+(4-1)\) \((1+2+5+11)+(5-1)\)

  可以看出,對於第 \(i(2 \leq i )\) 層的樹枝長,其實是等於前 \(i-1\) 層樹枝的長度之和與 \(i-1\)

的和的。看圖更容易發現這一規律:

  這裡第 \(3\) 層的樹枝和前兩層的樹枝和節點有一一對應的關係(紅色實線),可以看出長度恰好就是前兩層節點數2加上前兩層的樹枝長度,\(O(n)\) 遞推可以得到樹枝長度陣列,記為 \(len\)

同層節點規律

  觀察可知除了第一層外的其他層的同層相鄰節點距離是一定的。所以確定每一層第一個節點的位置就可以推出其他節點的位置了。
  讓第一層第一個節點水平位置為 \(1\) 。再次觀察前面的圖,可以發現\(i\) 層第一個節點的水平位置其實就是 \(len_i+1\) 。所以根據前面推出來的樹枝長陣列可以推出。豎直位置就得從根節點(也就是第 \(m\) 層)往下推了,根節點豎直位置為 \(1\)\(i\) 層豎直位置就其實就是 \(=\)\(i+1\) 層豎直位置 \(+\)\(i\) 層的樹枝長度 \(+1\) ,也是比較明顯的。我程式碼中將兩個方向的位置分別用 \(pos\)\(h\) 表示了。以下是初始化函式和一些陣列定義:

const int N = 3100;
int len[20],m,n,pos[20],h[20];
char a[N][N];  //滿二叉樹陣列,注意開大一點
void prepare(){
    int sum = 1;            //記錄樹枝長的字首和
    len[1] = 1;pos[1] = 1;  //第一層樹枝長為1,第一個節點水平位置為1
    FOR(i,2,m) {
        len[i] = sum + i-1; //遞推式子
        sum += len[i];
        pos[i] = len[i] + 1;//順便得到第i層第一個節點的水平位置
    }
    h[m] = 1;
    for(int i = m-1; i ;i --) h[i] = h[i+1]+len[i]+1;//得到第i層的豎直位置
    memset(a,' ',sizeof(a)); //全都鋪滿空格
}

  第一層節點的分佈已在題目中確定了,相鄰節點是兄弟就隔 \(3\) 個,不是隔 \(1\) 個,因為與其他層分佈不同,是要特判的。其他層結點間距也是很好找到規律的,就是 \(2 \times len_i+1\) 。至此,我們這棵樹的資訊基本完備了,下面就是比較輕鬆的繪製和刪點了。

繪製和刪點

  這兩個操作都是遞迴進行的。
  因為我們已經知道了每一層的樹枝長度,所以我們可以從根節點開始建樹,遞迴左右子樹即可。注意我們定義的樹枝長度為連線第 \(i\) 層節點與第 \(i+1\) 層節點的樹枝長度。程式碼採用了前序遍歷的方式:

void draw(int x,int y,int depth){
    a[x][y] = 'o'; //畫節點
    if(depth == 1) return;  //到葉子節點了,返回
    //開始畫樹枝,lx,ly定位左樹枝,rx,ry定位右樹枝
    int lx = x+1,ly = y-1,rx = x+1,ry = y+1;
    FOR(i,1,len[depth-1]){//注意畫的樹枝長度為下一層的樹枝長度
        a[lx][ly] = '/';
        a[rx][ry] = '\\';
        lx = lx+1,ly = ly-1,rx = rx+1,ry = ry+1;
    }
    draw(lx,ly,depth-1);   //畫下一層節點
    draw(rx,ry,depth-1);
}

  刪點比較暴力,注意刪點要同時刪除與父親節點的聯絡和與孩子節點的聯絡:

void destroy(int x,int y){
    a[x][y] = ' ';           //將該點置為空格
    if(a[x-1][y-1] == '\\') destroy(x-1,y-1);         //左上角
    if(a[x-1][y+1] == '/')  destroy(x-1,y+1);         //右上角
    if(a[x+1][y-1] == '/' || a[x+1][y-1] == 'o') destroy(x+1,y-1); //左下角,因為往下還要刪除孩子節點,要多一個判斷
    if(a[x+1][y+1] == '\\'|| a[x+1][y+1] == 'o') destroy(x+1,y+1); //右下角同理
}

一些可能阻止你AC的坑

  • 陣列大小要開大一點。滿二叉樹最大層數為 \(10\) ,葉子結點的豎直為置最大為 \(768\),該層寬度為 \(3072\) 。所以陣列大小應至少開到 \(769*3073\) 。否則可能出現 \(\mathbf{Too~ long~ on~ line~ 1}.\) 或者直接 \(\mathbf{RE}\) 等錯誤。
  • 陣列定義比較多,要用一些比較清晰的變數名,並且時刻記得它們的意義。
  • \(10\) 個點有點玄學。如果用快讀會 \(\mathbf{TLE}\) 掉,因為資料量小,全都用 \(\mathbf{cin}\) 就可以過了。
    \(Code:\)
#include <bits/stdc++.h>
#define FOR(i,a,b) for(int i = a;i <= b;i++)
using namespace std;
const int N = 3100;
int len[20],m,n,pos[20],h[20];
char a[N][N];  //滿二叉樹陣列,注意開大一點
int read(){int sum = 0,fu = 1;char ch = getchar();while(!isdigit(ch)){if(ch == '-')fu = -1;ch = getchar();}while (isdigit(ch)){sum=(sum<<1)+(sum<<3)+(ch^48);ch = getchar();}return sum*fu;}
//預處理
void prepare(){
    int sum = 1;            //記錄樹枝長的字首和
    len[1] = 1;pos[1] = 1;  //第一層樹枝長為1,第一個節點水平位置為1
    FOR(i,2,m) {
        len[i] = sum + i-1; //遞推式子
        sum += len[i];
        pos[i] = len[i] + 1;//順便得到第i層第一個節點的水平位置
    }
    h[m] = 1;
    for(int i = m-1; i ;i --) h[i] = h[i+1]+len[i]+1;//得到第i層的豎直位置
    memset(a,' ',sizeof(a)); //全都鋪滿空格
}

//繪製
void draw(int x,int y,int depth){
    a[x][y] = 'o'; //畫節點
    if(depth == 1) return;  //到葉子節點了,返回
    //開始畫樹枝,lx,ly定位左樹枝,rx,ry定位右樹枝
    int lx = x+1,ly = y-1,rx = x+1,ry = y+1;
    FOR(i,1,len[depth-1]){ //注意畫的樹枝長度為下一層的樹枝長度
        a[lx][ly] = '/';
        a[rx][ry] = '\\';
        lx = lx+1,ly = ly-1,rx = rx+1,ry = ry+1;
    }
    draw(lx,ly,depth-1);   //畫下一層節點
    draw(rx,ry,depth-1);
}

//刪點
void destroy(int x,int y){
    a[x][y] = ' ';           //將該點置為空格
    if(a[x-1][y-1] == '\\') destroy(x-1,y-1);         //左上角
    if(a[x-1][y+1] == '/') destroy(x-1,y+1);          //右上角
    if(a[x+1][y-1] == '/' || a[x+1][y-1] == 'o') destroy(x+1,y-1); //左下角,因為往下還要刪除孩子節點,要多一個判斷
    if(a[x+1][y+1] == '\\'|| a[x+1][y+1] == 'o') destroy(x+1,y+1); //右下角同理
}

//列印
void print(){
    int height = h[1];          //第一層的豎直位置
    int width = 6 * (1<<(m-1)); //第一層的寬度(最寬)
    FOR(i,1,height){
        FOR(j,1,width)
            printf("%c",a[i][j]);
        printf("\n");
    }
}

signed main(){
    m = read();n = read();
    prepare();
    draw(1,pos[m],m); //(1,pos[m])為根節點座標,位於第m層
    while(n--){
        int i = read(),j = read();
        if(i > 10) continue;
        int x = h[m+1-i],y; //因為層的定義與題目不同,得轉化一下
        //分第一層和其他層兩種情況計算水平位置y
        if(i == m){
            if(j & 1) y = pos[1] + j/2*6;
            else y = pos[1] + j/2*6 - 2;
        }
        else y = pos[m+1-i] + (j-1)* (2 * len[m+1-i] + 2); //可以手推
        destroy(x,y);
    }
    print();
    return 0;
}

  如果你想練習一下類似的畫圖題,以下兩題可以做做看: