NOTICE

 任何跟文章無關的閒聊,請愛用 留言板(Guestbook)

 想要快速瀏覽主題,請點選單 目錄 標籤。

 停止更新ing,請見諒。 <(_ _)>


4月 30, 2008

【演算】大數運算 Part 3

@
  緊接著加法與減法之後,讓我們來看看,該怎麼做大數乘法的運算。


  一開始實作乘法運算時,我使用的是最直觀的想法:假設要求出 A * B 之值,則先將 A 乘以 B 的第一位,再加上 A 乘以 B 的第二位乘以 10,再加上 A 乘以 B 的第三位乘以 102,......,再加上 A 乘以 B 的第 n 位乘以 10n-1。然後實際執行測試時,兩個整數只要超過九位數程式就跑不動了!

  該怎麼辦呢?顯然我們必須將演算原理作必要的簡化。仔細再將這個方法分析一下,可以發現:若假設 A * B = C,則 C 的第一位會等於 A 的第一位乘以 B 的第一位;C 的第二位會等於 A 的第一位乘以 B 的第二位加上 A 的第二位乘以 B 的第一位;C 的第三位會等於 A 的第一位乘以 B 的第三位加上 A 的第二位乘以 B 的第二位加上 A 的第三位乘以 B 的第一位,......。

  讓我們整理一下,設 A, B, C 的第 i 位分別為 ai, bi, ci,就可以得出以下公式:




  最後還有個問題,若是 n 位的 A 與 m 位的 B 相乘之後所得的 C 總共有幾位?最簡單的方法,就是兩者的最小可能值與最大可能值。C的位數就會落在 A 的最小可能值與 B 的最小可能值相乘的位數,和 A 的最大可能值與 B 的最大可能值相乘的位數之間。

  有點抽象?我們舉個例:若 A 有 5 位,B 有 3 位,則 A 的最小可能值為 10000,B 的最小可能值為 100,兩者相乘為 7 位;A 的最大可能值為 99999,B 的最大可能值為 999,兩者相乘為 8 位。所以可以得知 C 會落在 7 到 8 位之間。

  多做幾次試驗,就可以發現:n 位的 A 與 m 位的 B 相乘之後所得的 C,其位數會介於 n + m - 1 到 n + m 之間。


  整個乘法原理就差不多這樣了。其實作的虛擬碼大致如下:

void mul(large A, large B)
{
    Index i, j;
    int carry = 0;

    for (i = 1 to A 的位數 + B 的位數)
    {
        int n = carry;

        for (j = 1 to i)
        {
            n = n + A 的第 j 位 * B 的第 i + 1 - j 位;
        }

        C 的第 i 位 = n % 10;
        carry = n / 10;
    }

    while (carry != 0)
    {
        C 的第 i 位 = carry % 10;

        carry = carry / 10;
        i = i + 1;
    }

    print C;
}

To Be Continued . . .

4月 29, 2008

【演算】大數運算 Part 2

@
  決定大數的儲存方式後,接下來要怎麼運算呢?由於全部解釋完實在太佔篇幅,又因為加法與減法其實是一體兩面的(減一個正數就是加一個負數),讓我們姑且先解釋大數的加減法。


  大數的加法原理,有點像是一個十進位版的全加器。所有數字的第 n 位相加,再加上第 n - 1 位的進位數,就是答案的第 n 位數字。若有進位,則將此數作為第 n + 1 位加法的輸入。若是減法,則同樣是將所有數字的第 n 位相減。假如此數字差小於零,就跟第 n + 1 位數字"借"數。也就是將此數加 10,並將下一位數減 1 (還記得國小學過的減法定理嗎?)。

  以上兩種的虛擬碼實作出來大致如下:

void add(large A, large B)
{
    Index i;
    int carry = 0;

    for (i = 1 to maximun(A 的位數, B 的位數))
    {
        int n;

        n = A 的第 i 位 + B 的第 i 位 + carry;
        C 的第 i 位 = n % 10;
        carry = n / 10;
    }

    if (carry != 0)
    {
        C 的第 i 位 = carry;
    }

    print C;
}

void sub(large A, large B)
{
    Index i;
    int borrow = 0;

    for (i = 1 to maximun(A 的位數, B 的位數))
    {
        int n;

        n = A 的第 i 位 - B 的第 i 位 - borrow;
        if (n < 0)
        {
            n = n + 10;
            borrow = 1;
        }
        else
        {
            borrow = 0;
        }

        C 的第 i 位 = n;
    }

    print C;
}

※注意:在這裡,我們一律假設是 A, B ≧ 0。且在 sub() 函式中的 A ≧ B,因此結果並不會出現負數。以下會繼續說明。


  不同於電腦為了用加法表示減法,把"減一個數字"當成"加一個該數字的負數"。由於我的正負號是交由布林植表示,實際代表數字的向量並不能有負數。為了省去處理負號的麻煩,因此我的作法剛好相反,希望能以"正數之間的相加減",代表所有情況(不管哪個數是正數、哪個數是負數)的加減法,必要時再改變結果的正負號。

  以加法為例,我把兩數相加:a + b 分成以下六種情況:



  同樣的,兩數相減:a + b 也可以分成六種情況:



  可能有點複雜,不過若仔細看可以發現,我把負數(< 0)的數字全都"正數化"了。並且可以確定,在括號(包含中括號與小括號)中的數字必為正數。

  這樣做有什麼好處呢?

  例如加法只要在第二種與最後兩種情況中,我們可以確定結果必定為負數。因此只要出現這種情況,我們只要把算式做適當的轉換(例如在 a, b < 0 的情況下,將 a + b 轉換成 -[(-a) + (-b)]),就可以確定運算過程中,純數字部份(向量儲存的內容)必不會出現負數,並且因為在早已知道正負號的情況,可以直接設定代表正負號的布林變數。

To Be Continued . . .

4月 28, 2008

【演算】大數運算 Part 1

@
  在之前的作品:大數運算中,提供了我所做的大數(large)的類別函式庫,但是並沒有仔細去深入「大數運算」這個內容。在發表96年的資訊學科試題:序列長度問題解題時,我也只是將這個類別函式庫拿來用而已。

  因此,我承諾:要在這篇文章中,將大數做個比較詳盡的介紹。由於怕一次篇幅太大(尤其又是字多圖少),並且要考慮我的撰寫速度(似乎這才是主因),內容我會分成幾段、分天張貼出來。


  在一切開始之前,我們先來認識一下,什麼叫做大數?

  由於要有效使用記憶體,許多程式語言的不同資料型態,其變數大小都是固定的。以C/C++來說,字元(char)就是1byte、整數(int)不是2byte(16位元環境)、4byte(32位元環境,目前佔多數)就是8byte(64位元環境),浮點數就是4byte。

  因此,變數能夠表達的範圍都是固定的。以4byte的整數來說,能表達的就只有232個數字。就算是目前C/C++中整數表達範圍最廣的Long型態,能表達的也只有264個數字。

  運用數學對數的原理來看,對於序列長度問題122位的整數運算,顯然這些現有的資料型態已不敷使用。而為了解決這種問題,所做的"擴充整數"演算,就稱之為超長整數運算,或是大數運算。


  一般而言,解決這種問題的方式是使用陣列(array),利用文字拼湊的方式"拼"出一個大數來。不過實際上資料結構與運算的實作細節,其實可以說是各有不同的。

  在之前有提過,我所使用的資料結構就不是一般的陣列,而C++標準樣板函式庫(STL)的容器:字元(char)型態的向量(vector)。每一個字元代表的是大數的每一位數。除此之外,再使用一個額外的布林(boolean)變數代表大數的正負號。如圖所示:



To Be Continued . . .

4月 27, 2008

【解題】Ecological Bin Packing

@
ACM Volume I 102 - Ecological Bin Packing


Background

Bin packing, or the placement of objects of certain weights into different bins subject to certain constraints, is an historically interesting problem. Some bin packing problems are NP-complete but are amenable to dynamic programming solutions or to approximately optimal heuristic solutions.

In this problem you will be solving a bin packing problem that deals with recycling glass.


The Problem

Recycling glass requires that the glass be separated by color into one of three categories: brown glass, green glass, and clear glass. In this problem you will be given three recycling bins, each containing a specified number of brown, green and clear bottles. In order to be recycled, the bottles will need to be moved so that each bin contains bottles of only one color.

The problem is to minimize the number of bottles that are moved. You may assume that the only problem is to minimize the number of movements between boxes.

For the purposes of this problem, each bin has infinite capacity and the only constraint is moving the bottles so that each bin contains bottles of a single color. The total number of bottles will never exceed 231.


The Input

The input consists of a series of lines with each line containing 9 integers. The first three integers on a line represent the number of brown, green, and clear bottles (respectively) in bin number 1, the second three represent the number of brown, green and clear bottles (respectively) in bin number 2, and the last three integers represent the number of brown, green, and clear bottles (respectively) in bin number 3. For example, the line 10 15 20 30 12 8 15 8 31

indicates that there are 20 clear bottles in bin 1, 12 green bottles in bin 2, and 15 brown bottles in bin 3.

Integers on a line will be separated by one or more spaces. Your program should process all lines in the input file.


The Output

For each line of input there will be one line of output indicating what color bottles go in what bin to minimize the number of bottle movements. You should also print the minimum number of bottle movements.

The output should consist of a string of the three upper case characters 'G', 'B', 'C' (representing the colors green, brown, and clear) representing the color associated with each bin.

The first character of the string represents the color associated with the first bin, the second character of the string represents the color associated with the second bin, and the third character represents the color associated with the third bin.

The integer indicating the minimum number of bottle movements should follow the string.

If more than one order of brown, green, and clear bins yields the minimum number of movements then the alphabetically first string representing a minimal configuration should be printed.

Sample Input

1 2 3 4 5 6 7 8 9
5 10 5 20 10 5 10 20 10


Sample Output

BCG 30
CBG 50


解題思考

  看到這一題,我ㄧ開始的想法是:先找出這9個數字的最大值,再找出未被選擇的顏色與桶子中的最大值,接著同樣再找出未被選擇的顏色與桶子中的最大值。然後瓶子總和減掉這三個數字就是移動的最小值了。

  不過看到題目,假如出現同樣的最小值,則輸出字典順序最小的組合(間接的告訴你這是重點?)。再看到顏色順序是B-G-C,顯然我這方式是行不通的,因為最後必定會出現順序錯誤的問題。

  讓我們稍微轉個角度,既然顏色的順序是麻煩,就直接根據顏色順序來選。也就是說,直接照著字母順序,先選Clear要使用哪個桶子,再選Brown要使用哪個桶子,而最後那個桶子就是Green的。

  在這裡,我使用暴力法求出每種可能,再得到之中的最小值。由於已經是依照字母順序選擇,假如出現相同的最小值,則後得出的字典順序必小於先得到的。因此,如此便不用擔心組合順序的問題。


參考解答(C++)

#include <iostream>
#include <limits.h>

using namespace std;

bool select(int);

int bin[9], used[3], minn, n;
char pack[3];
const char color[] = {'B', 'C', 'G'};
const int order[] = {0, 2, 1};

int main(void)
{
    int get;

    // 確定能不能抓到第一筆資料
    while (cin >> get)
    {
        // 變數初始化
        n = get;
        minn = INT_MAX;
        for (int i = 0; i < 3; i++)
        {
            used[i] = false;
        }

        bin[0] = get;
        for (int i = 1; i < 9; i++)
        {
            cin >> bin[i];
            n += bin[i];
        }

        // 呼叫三層遞迴的第一層
        select(0);

        cout << pack[0] << pack[1] << pack[2] << " " << minn << endl;
    }

#ifndef ONLINE_JUDGE
    system("pause");
#endif
}

bool select(int m)
{
    bool ok = false;

    for (int i = 0; i < 3; i++)
    {
        if (used[i]) { continue; }

        n -= bin[i * 3 + order[m]];
        used[i] = true;

        // 假如還沒選到最後一個桶子
        if (m < 2)
        {
            if (select(m + 1))
            {
                pack[i] = color[m];
                ok = true;
            }
        }

        // 假如是目前最小值
        else if (n < minn)
        {
            minn = n;
            pack[i] = color[m];
            ok = true;
        }

        used[i] = false;
        n += bin[i * 3 + order[m]];
    }

    return ok;
}

4月 26, 2008

【解題】The Blocks Problem

@
ACM Volume I 101 - The Blocks Problem


Background

Many areas of Computer Science use simple, abstract domains for both analytical and empirical studies. For example, an early AI study of planning and robotics (STRIPS) used a block world in which a robot arm performed tasks involving the manipulation of blocks.

In this problem you will model a simple block world under certain rules and constraints. Rather than determine how to achieve a specified state, you will ``program'' a robotic arm to respond to a limited set of commands.


The Problem

The problem is to parse a series of commands that instruct a robot arm in how to manipulate blocks that lie on a flat table. Initially there are n blocks on the table (numbered from 0 to n-1) with block bi adjacent to block bi+1 for all 0 ≦ i < n-1 as shown in the diagram below:



The valid commands for the robot arm that manipulates blocks are:

  * move a onto b

   where a and b are block numbers, puts block a onto block b after returning any blocks that are stacked on top of blocks a and b to their initial positions.

  * move a over b

   where a and b are block numbers, puts block a onto the top of the stack containing block b, after returning any blocks that are stacked on top of block a to their initial positions.

  * pile a onto b

   where a and b are block numbers, moves the pile of blocks consisting of block a, and any blocks that are stacked above block a, onto block b. All blocks on top of block b are moved to their initial positions prior to the pile taking place. The blocks stacked above block a retain their order when moved.

  * pile a over b

   where a and b are block numbers, puts the pile of blocks consisting of block a, and any blocks that are stacked above block a, onto the top of the stack containing block b. The blocks stacked above block a retain their original order when moved.

  * quit

   terminates manipulations in the block world.

Any command in which a = b or in which a and b are in the same stack of blocks is an illegal command. All illegal commands should be ignored and should have no affect on the configuration of blocks.


The Input

The input begins with an integer n on a line by itself representing the number of blocks in the block world. You may assume that 0 < n < 25.

The number of blocks is followed by a sequence of block commands, one command per line. Your program should process all commands until the quit command is encountered.

You may assume that all commands will be of the form specified above. There will be no syntactically incorrect commands.


The Output

The output should consist of the final state of the blocks world. Each original block position numbered 0 ≦ i ≦ n where n is the number of blocks) should appear followed immediately by a colon. If there is at least a block on it, the colon must be followed by one space, followed by a list of blocks that appear stacked in that position with each block number separated from other block numbers by a space. Don't put any trailing spaces on a line.

There should be one line of output for each block position (i.e., n lines of output where n is the integer on the first line of input).

Sample Input

10
move 9 onto 1
move 8 over 1
move 7 over 1
move 6 over 1
pile 8 over 6
pile 8 over 5
move 2 over 1
move 4 over 9
quit


Sample Output

0: 0
1: 1 9 2 4
2:
3: 3
4:
5: 5 8 7 6
6:
7:
8:
9:


解題思考

  這一題不會太難,只要掌握題目即可順利完成。

  首先,我先使用一個n*n的陣列去存整個資料內容,再使用兩個大小為n的陣列儲存編號 i (0 ≦ i < n)積木在第幾行、第幾列。另外還有一個陣列表示每一堆的頂端。

  接著分析指令時,只要有 "move" 與 "onto" 這兩種指令,就需要把其上的積木歸位。所以我獨立出一個"歸位(returned)"的函式,供這兩種指令呼叫。

  之後的工作,就只是把 a 上的積木移動到 b 之上,要實現也不會太難。只要注意到題目所說,若 a = b 或是 a 與 b 在同一堆中,就不能執行任何動作。一開始我就是疏忽掉這一點了,難怪錯誤頻頻,害我百思不得其解。

  除了這種解法之外,我發現有人是使用串列(List)來解決這題。也就是說,一個積木的資料中,有一個指標指向其後的積木。這樣搬移積木時,只需要更改少部分的指標即可,可以省下許多搬移陣列元素的時間(我的陣列解法所需執行時間,約是此種方法的兩倍)。

  由於是別人寫的,這裡我就不貼出來了。


參考解答(C++)

#include <iostream>

using namespace std;

void returned(int);
void moved(int, int);

int **block, *col, *row, *top;

int main(void)
{
    int n;

    while (cin >> n)
    {
        // 動態配置記憶體, 並賦予初值
        col = new int[n];
        row = new int[n];
        top = new int[n];

        block = new int *[n];
        for (int i = 0; i < n; i++)
        {
            row[i] = 0;
            col[i] = i;

            top[i] = 1;

            block[i] = new int[n];
            block[i][0] = i;
        }

        while (1)
        {
            int a, b;
            char command[2][5];

            // 判斷輸入是否結束
            cin >> command[0];
            if (!strcmp(command[0], "quit")) { break; }

            cin >> a >> command[1] >> b;

            if (a != b && col[a] != col[b])
            {
                // 分析指令
                if (!strcmp(command[0], "move")) { returned(a); }

                if (!strcmp(command[1], "onto")) { returned(b); }

                moved(a, b);
            }
        }

        // 印出執行結果
        for (int i = 0; i < n; i++)
        {
            cout << i << ":";

            for (int j = 0; j < top[i]; j++)
            {
                cout << " " << block[i][j];
            }

            cout << endl;
        }

        // 釋放記憶體空間
        delete [] col;
        delete [] row;
        delete [] top;

        for (int i = 0; i < n; i++) { delete [] block[i]; }
        delete [] block;
    }

#ifndef ONLINE_JUDGE
    system("pause");
#endif
}

void returned(int a)
{
    int x = col[a];

    // 將 block 之上全部歸位
    for (int i = row[a] + 1; i < top[x]; i++)
    {
        int n = block[x][i];

        block[n][top[n]] = n;

        row[n] = top[n]++;
        col[n] = n;
    }

    // 更新此行頂端
    top[x] = row[a] + 1;
}

void moved(int a, int b)
{
    int x = col[a], y = col[b], m = row[a];

    // 依序移動 a 到 b 上
    for (int i = 0; i < top[x] - m; i++)
    {
        int n = block[y][top[y] + i] = block[x][m + i];

        col[n] = y;
        row[n] = top[y] + i;
    }

    // 更新頂端
    top[y] += top[x] - m;
    top[x] = m;
}

4月 25, 2008

【目錄】北市高中資訊競賽

@
台北市高中資訊學科能力競賽 - 術科試題解題一覽


95學年度

第一題:售票系統 (Sales)
第二題:函數計算 (Comp)
第三題:井字遊戲 (TTT)
第四題:用餐地點 (Lunch)
第五題:最大矩形 (Area)
第六題:送愛心到肯大亞 (Care)


96學年度

第一題:積木的拼疊問題 (Bricks)
第二題:錄製專輯 (Record)
第三題:會議中心 (Room)
第四題:序列長度問題 (Sequence)
第五題:樹狀結構展示 (Tree)

【解題】樹狀結構展示 - Tree

@
台北市96學年度高中資訊學科能力競賽 - 樹狀結構展示 (Tree)


問題描述

  樹狀結構為一種程式設計者常用的資料結構,但較不容易在畫面上展示。常見的樹狀結構展示方式為「樹根在左、樹枝朝右」的橫向目錄型式,但其實若能以「樹根在上、樹枝朝下」的直向表示法,更能展現樹狀結構的精神。而要在電腦的二維螢幕上畫出美觀的樹狀結構,必須計算其中各個節點的座標位置。以下圖的樹狀結構為例,在一般文字模式的螢幕上(一個位置一個字元),其畫出來的樣子如下所示。其中名稱為N-1的節點,其起始座標為(5, 3),名稱為N-2的節點,其起始座標為(22, 3),名稱為N-1-1的節點,其起始座標為(1, 6),其餘依此類推。



  根據上例的表示方法,請寫一程式讀入如下格式的樹狀結構後,轉換為直向的樹狀結構,輸出每個節點的二維座標位置。

  在將輸入轉換成直向的樹狀圖時,為求美觀,轉換時必須符合下列規則:
   1. 節點以其名稱輸出。所有的距離、位置、寬度,都以「字元數」計算。
   2. 同一階層的節點有相同垂直位置。
   3. 同一階層的節點間必須求出最小間隔,但至少距離一個空白字元。
   4. 若某一節點有多個子節點,則其水平位置必需在所有子節點的總寬度的正中央。其計算公式為:(LS + LE + L) / 2。

  其中:
   S:本身節點的起始水平位置;當S 無法整除時,取其四捨五入值。
   LS:第一個子節點的起始水平位置。
   LE:最後一個子節點的最後水平位置。
   L:本身子節點的長度。


輸入檔格式(C:\tree\input.txt)

  1. 輸入為多行字串,每一行代表一個由根節點到本身節點的路徑,節點名稱由英文、數字與可列印符號表示,而上、下節點之間以冒號隔開。

  2. 所有的輸入依照深度優先演算法(Depth-First)列出,不會有如下情形:
   N-1
   N-1:N-1-1
   N-2
   N-1:N-1-2


  3. 輸入格式一定正確,不需考慮輸入格式錯誤情形。


輸出檔格式 (C:\tree\output.txt)

  1. 輸出時,先輸出每個節點的路徑,其順序輸入相同。

  2. 每個路徑後面,空一格後加上括號,括號內第一個值為該節點的起始水平座標位置,第二個值為其垂直座標位置。

  3. 水平與垂直座標位置均以字元數計算,由1 開始。如上例中的節點N-1,其起始水平座標位置為5,則輸出5,垂直座標為3,則輸出3。


輸入檔範例1

N-1
N-1:N-1-1
N-1:N-1-2
N-1:N-1-2:N-1-2-1
N-2
N-2:N-2-1
N-2:N-2-1:N-2-1-1
N-2:N-2-2
N-2:N-2-3


輸出檔範例1

N-1 (5,3)
N-1:N-1-1 (1,6)
N-1:N-1-2 (7,6)
N-1:N-1-2:N-1-2-1 (6,9)
N-2 (22,3)
N-2:N-2-1 (15,6)
N-2:N-2-1:N-2-1-1 (14,9)
N-2:N-2-2 (21,6)
N-2:N-2-3 (27,6)


輸入檔範例2

A
A:AB
A:AC
a
a:ab
a:ac


輸出檔範例2

A (3,3)
A:AB (1,6)
A:AC (4,6)
a (9,3)
a:ab (7,6)
a:ac (10,6)


解題思考

  這題老實說比較麻煩,我的作法是把問題分成兩個部份:儲存輸入資料,以及處理解析後的資料(也就是得出題目要求的節點座標)。

  要如何根據輸入,將必要的資料儲存在變數呢?在一次讀取檔案的一整行之後,根據題意,我們把這筆資料根據":"作字串切割。這些切割出來的內容,就是一個節點的名稱以及它的節點路徑。

  假若這一行的字串共切割成n個部份,則第1個到第n-1個部份即為節點路徑,而最後一個部份就是節點名稱了。至於資料的儲存,就只要根據切割出來的節點路徑,找到其父節點並新增節點資料就可以了。

  現在,我們已經能將資料正確的存入結構中了。接著要怎麼得知節點座標呢?

  首先,因為每一階層的節點都需要有最小間隔,且至少要距離一個空白,所以我們需要一些變數來存取每個階層的"邊界"。接著,我們就照著深度優先、遞迴的方式,來放置每個節點。

  我們舉題目的範例1來看,請看下圖:



  第一層目前的邊界為0,因此節點"N-1"暫時放置在第一格的位置,並將邊界加上L+1(節點長度+間隔的空格)。



  第二層目前的邊界為0,因此節點"N-1-1"暫時放置在第一格的位置,並將邊界加上L+1。所以"N-1-2"放置於第七格,並同樣再將邊界加上L+1。



  第三層目前的邊界為0,因此節點"N-1-2-1"暫時放置在第一格的位置,並將邊界加上L+1。



  由"N-1-2-1"節點的位置回推"N-1-2"的節點位置(利用題目給予的公式):結果小於最小間隔,故將"N-1-2-1"節點調整到配合"N-1-2"節點的位置。



  由"N-1-1"與"N-1-2"節點的位置回推"N-1"的節點位置:結果大於其目前的位置,故將"N-1"節點調整到配合"N-1-1"與"N-1-2"節點的位置。

  只要能讓程式重複以上步驟,就能夠達成題目的要求了。最後再利用深度優先的方式輸出結果,這題就完成了!


參考解答(C++)

#include <iostream>
#include <fstream>

#include <string>
#include <vector>

#define _GET_MAX_ 256

using namespace std;

struct node
{
    int s; // 座標起始點
    string name; // 節點名稱
    vector<node> childen; // 子節點
};

int move(node &, int);
void show(node &, int, string, ofstream &);

vector<int> border;

int main(void)
{
    ifstream in("C:/tree/input.txt");
    ofstream out("C:/tree/output.txt", ios_base::trunc);

    // 建立一個"虛擬根"作存取起始點
    node root;
    root.s = 0;

    cout << "讀入資料 . . ." << endl;
    char get[_GET_MAX_];
    while (in.getline(get, _GET_MAX_))
    {
        // 建立一個節點指標, 指向根節點
        node *tree = &root;

        string str = get;

        string sub;
        int start = 0, end;
        while (1)
        {
            // 根據冒號擷取資料
            end = str.find(":", start);
            sub = str.substr(start, end - start);

            // 路徑結束, 跳出
            if (end == string::npos) { break; }

            // 尋找節點路徑
            for (vector<node>::iterator it = (*tree).childen.begin();
                it < (*tree).childen.end(); it++)
            {
                if ((*it).name == sub)
                {
                    // 更新節點指標為此節點
                    tree = &(*it);
                    break;
                }
            }

            start = end + 1;
        }

        // 新增一個節點, 使之為當前節點的子節點
        node newnode;
        newnode.name = sub;
        newnode.s = 1;

        (*tree).childen.push_back(newnode);
    }

    in.close();

    // 呼叫遞迴函式處理資料並印出結果
    move(root, 0);
    show(root, 0, "", out);
    out.close();

    cout << "輸出資料在 ouput.txt" << endl;

    system("pause");
}

int move(node &tree, int level)
{
    for (vector<node>::iterator it = tree.childen.begin();
        it < tree.childen.end(); it++)
    {
        // 此層級邊界不存在就新增一個
        if (border.size() <= level) { border.push_back(0); }

        // 保持節點間隔
        (*it).s += *(border.begin() + level);
        *(border.begin() + level) += (*it).name.size() + 1;

        // 假如有子節點
        if ((*it).childen.size())
        {
            // 呼叫遞迴函式得出偏移量, 以更新起始座標及邊界
            int p = move(*it, level + 1);

            (*it).s += p;
            *(border.begin() + level) += p;
        }
    }

    int m = ((*tree.childen.begin()).s + (*(tree.childen.end() - 1)).s
            + (*(tree.childen.end() - 1)).name.size() - tree.name.size()) / 2;

    if (m < tree.s)
    {
        // 偏移量為負, 修正子節點座標及邊界
        for (vector<node>::iterator it = tree.childen.begin();
            it < tree.childen.end(); it++)
        {
            (*it).s += tree.s - m;
        }

        *(border.begin() + level) += tree.s - m;

        return 0;
    }

    // 返回父節點偏移量
    return m - tree.s;
}

void show(node &tree, int level, string str, ofstream &out)
{
    if (level)
    {
        // 印出當前節點座標
        cout << str << tree.name << " (" << tree.s << ", " << 3 * level << ")" << endl;
        out << str << tree.name << " (" << tree.s << ", " << 3 * level << ")" << endl;

        str += tree.name + ":";
    }

    // 遞迴呼叫函式顯示結果
    for (vector<node>::iterator it = tree.childen.begin();
        it < tree.childen.end(); it++)
    {
        show(*it, level + 1, str, out);
    }
}

4月 21, 2008

【解題】序列長度問題 - Sequence

@
台北市96學年度高中資訊學科能力競賽 - 序列長度問題 (Sequence)


問題描述

  傑倫與伊林非常喜歡上高中生物課,而生物老師孔智慧有一個「序列長度」問題,希望兩位同學幫忙計算。孔老師觀察出昆蟲體內存有許多過去從未被發現的奇特結構序列,這些結構序列是由一組元素組成,而且每一個元素在每一個結構序列中最多僅會出現一次。例如,當有{A, B} 2個不同組成元素時,則可以組成A、B、A-B、B-A等4 種結構序列,這4種結構序列的長度總和為6。而當有{A, B, C} 3個不同組成元素時,則可以組成A、B、C、A-B、B-A、A-C、C-A、B-C、C-B、A-B-C,A-C-B、B-A-C、B-C-A、C-A-B、C-B-A等15種結構序列,而這15種結構序列的長度總和為33。由於預期陸續會有含新組成元素的結構序列被發現,孔老師因此希望傑倫與伊林幫忙計算看看當不同組成元素有n個時,所有可能的不同結構序列長度總和為何?聰明的你(妳)請寫一個程式幫傑倫與伊林來回答這個問題。


輸入檔格式(C:\sequence\input.txt)

  輸入一行只有一個正整數n(1≦n≦1000),代表不同組成元素。


輸出檔格式 (C:\sequence\output.txt)

  輸出一個整數,即所有可能結構序列的總長度。

註:第二組輸出範例為一個122位的正整數,在本範例中因列印問題已自動更改格式為四行輸出,但在您的輸出檔中,仍應輸出至同一行中。


輸入檔範例1

5


輸出檔範例1

1305


輸入檔範例2

80


輸出檔範例2

1536913041036158631879990235799326108728
2487463803004967244454894616548299221460
1682328139903912967663080782140497997968
80


解題思考

  又是一篇落落長,看得霧煞煞?沒關係,讓我們先將題目做個分析,一步一步來。

  首先,我們把結構序列依照長度做個區分。運用排列組合的知識,在有n種組成元素的情況下:長度為1的序列,有P n取1種可能,也就是C n取1 * n!;長度為2的序列,有P n取2種可能;長度為3的序列,有P n取3種可能......。

  以此類推,我們可以得出,題目要求的序列總長度能夠以一條公式表示:



  當然,只是這樣這題還不算解完。在題目輸出格式的附註中,有提到結果會出現一個122位的正整數。因此,我們還需要使用超長整數來做運算(這就是我之前提到的怨念)。

  請恕我在參考解答中,直接引括之前曾貼出的作品 - 大數運算來使用。之後,我會特別獨立一篇,將大數運算的四則運算原理作個解釋。在這裡我們先略過不談。


參考解答(C++)

#include <iostream>
#include <fstream>

#include "large.h"

using namespace std;

int main(void)
{
    ifstream in("C:/sequence/input.txt");
    ofstream out("C:/sequence/output.txt", ios_base::trunc);

    int n;
    large sum = 0;

    cout << "讀入資料 . . ." << endl;
    in >> n;
    in.close();

    cout << n << endl;

    for (int i = 1; i <= n; i++)
    {
        large t = 1;
        for (int j = n; j > n - i; j--) { t *= j; }

        sum += t * i;
    }

    out << sum << endl;
    out.close();

    cout << "總長度為 " << sum << endl;
    cout << "輸出資料在 ouput.txt" << endl;

    system("pause");
}

4月 19, 2008

【解題】會議中心 - Room

@
台北市96學年度高中資訊學科能力競賽 - 會議中心 (Room)


問題描述

  拼拼樂會議中心是一個N×N的超大型可分割式會議中心。每一個1×1的空間都可以用隔板隔開,因此該會議中心最多可以有n2個獨立的1×1會議室,如要較大的會議室,則需將隔板拿掉使得二或更多個相鄰的1×1空間可以合併使用。圖一的會議中心最多可分隔成169個1×1小會議室,最少則全部合併成為一個13×13的會議室。每間1×1會議室皆以其二維平面座標為編號。選定一個1×1會議室並給予編號(0, 0),相鄰的上、下、左、右會議室編號則依序為(0, 1), (0, -1), (-1, 0), (0, 1)。

  會議中心外租會議室時,必須按照下列規則,組成合乎需求的會議室。一開始先以編號為(0, 0)的空間供租用,如果空間不足,則依序向右方、上方、左方、下方的空間合併成為較大的會議室。每次擴充時,新加入的空間必須為正方形且該邊長必須與相鄰的擴充前會議室邊長相同,如此才能確保合併後的會議室一定是四方形。下圖為例,第一次擴充租用空間時,右邊編號為(1, 0)的會議室空間會被跟編號為(0, 0)的會議室合併。第二次擴充時,在(0, 0), (1, 0)上方的四個(2×2正方形)小會議室會被合併進來。第三次擴充時,在(0, 0)~(0, 3)左邊的9個 (3×3正方形)小會議室會被合併進來。第四次擴充時,在(-3, 0)~(1, 0)下方的25個 (5×5 正方形)小會議室會被合併進來。第五次擴充時,在(0, -5)~(0, 2)右方的64個 (8×8 正方形)小會議室會被合併進來。後續的擴充則依此類推。



  現在,若給定一個n的值,請計算第n次擴充時的正方形會議室的邊長。


輸入檔格式(C:\room\input.txt)

  輸入檔只有一個整數n,n≦45。


輸出檔格式 (C:\room\output.txt)

  請輸出第n次擴充時的正方形會議室的邊長。


輸入檔範例1

2


輸出檔範例1

2


輸入檔範例2

5


輸出檔範例2

8


解題思考

  這道題目說實在真是落落長。然而其中的重點其實只有一個:答案是一個斐波那契數列(Fibonacci sequence)!第一項是1、第二項是2、第三項是3、第四項是5、......。之後的每一項,都是其前兩項之和。

  說到這裡,這題不用多加解釋了吧!


參考解答(C++)

#include <iostream>
#include <fstream>

using namespace std;

int main(void)
{
    ifstream in("C:/room/input.txt");
    ofstream out("C:/room/output.txt", ios_base::trunc);

    int n, i;
    unsigned long t[2];

    cout << "讀入資料 . . ." << endl;

    in >> n;
    in.close();

    cout << n << endl;

    for (i = 0; i < n + 2; i++)
    {
        t[i % 2] = i < 2 ? 1 : t[0] + t[1];
    }

    out << t[i % 2] << endl;
    out.close();

    cout << "邊長為 " << t[i % 2] << endl;
    cout << "輸出資料在 ouput.txt" << endl;

    system("pause");
}

4月 17, 2008

【解題】積木的拼疊問題 - Bricks

@
台北市96學年度高中資訊學科能力競賽 - 積木的拼疊問題 (Bricks)


問題描述

  小明和同學一起組隊參加拼疊積木比賽,比賽規則如下:

   1. 由各隊隊員在限定時間內到運動場的沙地上尋找小積木。
   2. 利用找到的小積木,每二個對應為一組,拼成正方形或長方形的大積木。
   3. 看看那一隊找到的小積木所組成的大積木面積最大,即可獲勝。

  如果我們知道小明這一隊所找到小積木個數與它們的高度和長度,請你寫一個程式算出他們利用這些小積木可以拼出來的大積木的最大面積。

說明:

  1. 每個埋在沙中的小積木寬度(W)若為一單位,則長度(L)與高度(H)為一單位的整數倍,且長度與寬度的四個邊中至少有一個邊是整齊的,不會有凹凸的狀況。小積木為實心的,沒有挖洞,而且不會是正方形或長方形,下圖所示為一部份小積木的樣子。



  2. 若將小積木整齊的那一個邊對齊坐標軸L,則上圖小積木皆可用一串數字表示,這一串數字的個數等於小積木的長度,而每一個數字則代表其對應的高度。例如:小積木A = (1, 2, 2, 2, 2)、B = (1, 2, 1, 2, 1)、C = (2, 1, 2, 1, 2)、D = (4, 3, 2, 1)、E = (4, 1)、F = (3, 3, 1)、以及G = (2, 2, 2, 1, 1)。

  3. 小積木是可以利用旋轉或翻轉來組成正方形或長方形的大積木的,但所組成的大積木必需是實心的,也就是小積木與小積木的接觸面必需密合,中間不能有洞產生。例如上圖中的小積木B 與C 可以組成一個3×5 的長方形大積木,而E和G 則可以組成3×4 的長方形大積木。另外,如果有二個E 就可以組成5×2的長方形大積木,而二個D 可以組成5×4 的正方形大積木了。

條件限制:

  1. 小積木的長(L)寬(W)高(H)之範圍如下: W = 1、1 < L < 30、1 < H < 30。
  2. 小積木的個數最多50 個,它們的樣子是可以重覆出現的。
  3. 任何一個正方形或長方形的大積木只能由二個小積木組成,但小積木在組合時可以旋轉或翻轉。
  4. 若找到的小積木皆無法組成大積木,請輸出0。


輸入檔格式(C:\bricks\input.txt)

  1. 檔案第一行的數字代表小積木的個數。
  2. 檔案第二行以後,每一行皆由一串數字組成,數字間利用空白分隔,這一串數字表示一個小積木,表示方式如說明(2)。


輸出檔格式 (C:\bricks\output.txt)

  輸出的數字為由小積木組成的大積木的最大面積。


輸入檔範例1

5
1 2 1 2 1
2 1 2 1 2
4 3 2 1
4 3 2 1
1 3 2 1


輸出檔範例1

20


輸入檔範例2

4
4 1
1 4
1 1 1 2
4 1


輸出檔範例2

10


解題思考

  首先,我們先想想該如何儲存輸入的每個小積木。在這裡,我選擇使用一個整數陣列來存一個積木。陣列的大小即為積木的長度(L),而每個陣列元素,即為該列的高度(H)。

  然後看到題目,我們還需要讓小積木適當的旋轉、組合,使之成為一個實心方塊。因此,我們需要解決的幾個問題有:

   1.如何判斷兩塊小積木能否組成實心方塊。
   2.小積木該如何旋轉。
   3.兩塊小積木應該怎麼組合。

  該怎麼判斷兩塊積木能組合成題目要求的實心方塊?由於題目規定小積木皆為實心,因此我們可以直接利用兩塊積木的高度總和。假如兩塊積木的高度總和恆等,就代表兩塊積木能夠組合成實心方塊。

  例如下面這張圖。由於左邊的兩塊積木,組合之後的高度總和皆為 3,所以可以判斷出,這兩塊積木能夠組合成一塊 3 * 3 實心方體積木。反之,右邊兩塊積木的高度則不全相等,因此得知其無法組成一塊實心方塊。



  再來我們看到第二點。小積木的旋轉,其實就只是使之長度與高度互換。例如題目例圖的 E = (4, 1),旋轉後即變為 E' = (2, 1, 1, 1)。



  因為我們希望,在圖形旋轉後仍然可以用同樣的陣列表示法。也就是說,其朝下的邊必須維持整齊,且保持實心狀態。簡單來講,就是旋轉前必須確定,原圖形的高度是呈遞增或遞減的。例如題目圖形的 A、D、E、F、G;相對的,圖形 B 跟 C 則不可以旋轉。

  關於旋轉積木,我們只需要做到這個部份。或許你還會想到需要左右顛倒。例如圖形 D = (4, 3, 2, 1) 旋轉後即成為 D' = (1, 2, 3, 4)。不過關於這個部份,我把它合併到判斷實心方塊的部份了。這只是我的作法,僅供參考,你也可以將之獨立出來。

  最後,我們要思考,兩塊小積木應該怎麼組合?讓我們先這樣想:由於只需要靠高度總和,就可判斷兩個小積木能否組合,因此我們只需要考慮長度方向擺放的位置。

  至於兩個積木間總共有多少排法。先設積木 A 的長度為 LA,積木 B 的長度為 LB,則總排法應該會有 LA + LB - 2 種。假如不知道為什麼,可以畫出兩個高度皆為 1 的長方體積木,自己試看看能夠排幾種。


參考解答(C++)

#include <iostream>
#include <fstream>

#define _GET_MAX_ 256

using namespace std;

bool rotate(int *, int &);
int link(int *, int, int *, int);

int main(void)
{
    ifstream in("input.txt");
    ofstream out("output.txt", ios_base::trunc);

    int **brick, *len, n, bmax = 0;

    cout << "讀入資料 . . ." << endl;
    in >> n;
    cout << n << endl;

    // 動態規劃記憶體
    brick = new int *[n];
    len = new int[n];

    // 消除這行剩下的部份
    char get[_GET_MAX_];
    in.getline(get, _GET_MAX_);

    // 進入資料讀取迴圈
    for (int i = 0; i < n; i++)
    {
        char get[_GET_MAX_];
        in.getline(get, _GET_MAX_);

        int num = 0, top = 0;
        brick[i] = new int[30];
        for (int j = 0; j < strlen(get); j++)
        {
            if (get[j] >= '0' && get[j] <= '9')
            {
                num = num * 10 + get[j] - '0';
            }
            else if (num)
            {
                brick[i][top++] = num;
                cout << num << " ";
                num = 0;
            }
        }

        if (num)
        {
            brick[i][top++] = num;
            cout << num << " ";
        }

        len[i] = top;

        cout << endl;
    }

    in.close();

    for (int i = 0; i < n - 1; i++)
    {
        for (int j = i + 1; j < n; j++)
        {
            // 組合積木並求出面積
            int area = link(brick[i], len[i], brick[j], len[j]);

            if (!area && rotate(brick[i], len[i]))
            {
                area = link(brick[i], len[i], brick[j], len[j]);
            }

            if (!area && rotate(brick[j], len[j]))
            {
                area = link(brick[i], len[i], brick[j], len[j]);
            }

            bmax = max(bmax, area);
        }
    }

    for (int i = 0; i < n; i++) { delete [] brick[i]; }
    delete [] brick;
    delete [] len;
 
    out << bmax << endl;
    out.close();

    cout << "組合最大面積為 " << bmax << endl;
    cout << "輸出資料在 ouput.txt" << endl;

    system("pause");
}

bool rotate(int *brick, int &len)
{
    // 確認是否可以翻轉
    bool ok[2] = {true, true};
    for (int i = 1; i < len; i++)
    {
        if (ok[0] && brick[i] > brick[i - 1]) { ok[0] = false; }

        if (ok[1] && brick[i] < brick[i - 1]) { ok[1] = false; }

        if (!ok[0] && !ok[1]) { return false; }
    }

    int n = max(brick[0], brick[len - 1]);

    // 執行翻轉的動作
    int *b = new int[len];
    for (int i = 0; i < len; i++) { b[i] = brick[i]; }

    for (int i = 0; i < n; i++)
    {
        brick[i] = 0;

        for (int j = 0; j < len; j++)
        {
            if (b[j] > i) { brick[i]++; }
        }
    }

    // 更新積木長度
    len = n;

    return true;
}

int link(int *brick1, int len1, int *brick2, int len2)
{
    // 試著從各個位置組合
    for (int i = 1; i < len1 + len2; i++)
    {
        int t, s = min(len1, len2), b = max(len1, len2);

        // 判斷組合後的長度
        if (i < s)      { t = b + s - i; }
        else if (i > b) { t = i; }
        else            { t = b; }

        // 正反各測試一次
        for (int j = 0; j < 2; j++)
        {
            // 判斷是否可組合成方形
            int k, hei = 0;
            for (k = 0; k < t; k++)
            {
                int check = 0;

                // 分別加總每排的高
                if (i < len1)
                {
                    if (k < len1) { check += brick1[k]; }

                    int n = k - len1 + i;
                    if (n >= 0 && n < len2)
                    {

                        if (j)  { check += brick2[len2 - n - 1]; }
                        else    { check += brick2[n]; }
                    }
                }
                else
                {
                    int n = k + len1 - i;
                    if (n >= 0 && n < len1)
                    {
                        check += brick1[n];
                    }

                    if (k < len2)
                    {
                        if (j)  { check += brick2[len2 - k - 1]; }
                        else    { check += brick2[k]; }
                    }
                }

                // 無法組合, 跳出
                if (!hei) { hei = check; }
                else if (check != hei) { break; }
            }

            // 若成功組合則返回積木面積
            if (k == t) { return hei * t; }
        }
    }

    // 無法組合, 返回0
    return 0;
}

4月 13, 2008

【作品】Base64 編碼/解碼器

@
  由於Firefox網頁顯示快,擴充性又高(數不清的外掛),我本人算是Firefox的愛好者。奇怪了,這隻程式跟Firefox又有什麼關係呢?

  說老實話,其實可以說一點關係都沒有。不過當初,我在亂翻Firefox的資料夾時,發現Firefox的Search Plugins都是用XML寫的。

  我興致一發,就開始嘗試分析XML的架構。後來,在Image的標籤中,看到了以下的"未知文字":

R0lGODlhEAAQAJECAP8AAAAAAP///wAAACH5BAEAAAIALAAAAAAQA
BAAAAIplI+py+0NogQuyBDEnEd2kHkfFWUamEzmpZSfmaIHPHrRgu
Um/fT+UwAAOw==


  理論上,這應該是Search Plugins旁的小ICON。但是,對於這一長串東西,實在看不出是什麼。後來一問之下,才發現這是叫做Base64的編碼法。

  後來,就先把做Search Plugins的事丟在一旁。參考Wikipedia條目的編碼原理,試著把Base64的編碼/解碼程式寫出來,實現它。


程式說明

  這支程式只能使用命令提示工具執行。如下圖:



  程式的預設動作為編碼(Encode)。假如想改變預設動作為解碼(Decode),須輸入指令"/decode"。同理,若是想改回編碼,則須輸入指令"/encode"。

  你也可以一次幫很多檔案作編碼動作。只要圈選所有欲編碼的檔案,直接拖曳到程式圖示就可以了(由於一開始的預設動作都是編碼,因此無法做批次解碼)。

  此外,只有副檔名為.ba64的檔案能進行解碼。


程式下載

  

4月 12, 2008

【解題】送愛心到肯大亞 - Care

@
台北市95學年度高中資訊學科能力競賽 - 送愛心到肯大亞 (Care)


問題描述

  在非洲的友邦肯大亞由於久經乾旱、傳染病、戰亂等天災人禍摧殘,民不聊生、亟待外界物資救援,遠在台北的承緒與智鈴發起送愛心到肯大亞活動,已經募集一些文具用品準備分送到肯大亞各地城鎮的小朋友就學使用。但是肯大亞的交通建設非常落後,僅部分城鎮間有單方向可行駛的道路連結,並且在軍事管制下不是隨時都可通行,已知肯大亞所有省的鄉鎮數皆不超過 20 個。下圖是肯大亞某一省的鄉鎮道路地圖,簡單起見城鎮名稱用數字(1 ~ 20)來編號:



  其中每一點代表一個城鎮,每一個方向線代表城鎮間的道路連結與車輛通行方向。線上數值代表某天可以通行的機率。像是城鎮6 到8 的通行路徑僅有一種選擇,即是一條單方向可行駛道路連結 6 到 7 到 8,所以至少有一條路徑可通行的機率是 0.3 * 0.7 = 0.21。而城鎮 1 到 4 則有兩條路徑分別是:(1)先行經連結 1 到 2 道路(可通行的機率是0.7)再行駛連結 2 到 4 道路(可通行的機率是0.8);(2)或是直接行駛連結 1 到 4 道路(可通行的機率是0.8),所以至少有一條路徑可通行的機率是1 - (1 - 0.7 * 0.8)(1 - 0.8) = 0.912。

  已知送愛心到肯大亞活動每天會從某一省的一個城鎮出發運送救援物資的同一個省的另一個城鎮,車輛沿途最多僅會經過某一個城鎮一次。聰明的各位,請幫富有愛心的承緒與智鈴計算看看,在給定某一省的鄉鎮地圖與每條道路通行的機率情況下,任意兩城鎮間至少有一條路徑可通行的機率為何。


輸入檔格式(C:\area\input.txt)

  第 1 行為某一省城鎮數目N;接下來第 2 行到第 N + 1 行為一個 N * N 陣列,陣列的第 k 列 (row) (即輸入檔的第 k + 1 行)的每一個數值代表某一標號為 k 的城鎮到其它城鎮(1 至 N)間道路可通行的機率;輸入檔的最後一行(第 N + 2 行)有兩個數值分別(由左至右)代表起始城鎮編號與目的地城鎮編號。任一兩個數字之間都用一個空白隔開。


輸出檔格式 (C:\area\output.txt)

  從起始城鎮到目的地城鎮至少有一條路徑可通行的機率值(準確到小數點以後第五位)。


輸入檔範例1

1
1.0
1 1


輸出檔範例2

1.00000


輸入檔範例2

2
1.0 0.1
0.0 1.0
1 2


輸出檔範例3

0.10000


輸入檔範例3

8
1.0 0.7 0.9 0.8 0.0 0.0 0.0 0.0
0.0 1.0 0.0 0.8 0.6 0.0 0.0 0.0
0.0 0.0 1.0 0.0 0.0 0.8 0.0 0.0
0.0 0.0 0.0 1.0 0.0 0.7 0.0 0.6
0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.5
0.0 0.0 0.0 0.0 0.0 1.0 0.3 0.0
0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.7
0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
1 4


輸出檔範例1

0.91200


解題思考

  首先,從題目看來,我們需要用程式"走"過每一條由起始點到終點可能路徑,並計算出該路徑的通行機率。由於需要試著走過每條路徑,所以我採用遞迴的方式進行。每遞迴一層,就像是往下一個城鎮走。

  至於機率值怎麼計算呢?我們以一個變數值儲存"走某路線到當前節點"的機率,並設定初始值為 1 (由起點出發,在起點的機率當然就是 100% 囉)。每往下一個節點走(即呼叫一次遞迴)時,就乘上兩節點間的通行機率。反之,假如要往前回朔,則除以兩點間的機率。

  此外,仔細看題目,同一個城鎮不可以走第二次。因此,我們還需要紀錄哪些路徑已經走過了,在程式"走"的時候不要重複經過同一城鎮。

  最後再使用題目提供的,在有 n 條可行路徑的情況下:機率 = 1 - (1 - 路徑 1 的機率) * (1 - 路徑 2 的機率) * ... * (1 - 路徑 n 的機率),就能夠計算出結果。


參考解答(C)

#include <stdio.h>
#include <stdlib.h>

float **city, chance = 1, num = 1;
void care(int);
int *gone, start, end, n;

int main(void)
{
    int i, j;

    // 宣告檔案串流
    FILE *in, *out = fopen("C:/care/output.txt", "w+");
    if (!(in = fopen("C:/care/input.txt","r")))
    {
        printf("找不到 input.txt\n");
        system("pause");
        exit(1);
    }

    fscanf(in, "%d", &n);
    printf("%d\n", n);

    // 動態配置記憶體空間
    city = (float**)malloc(sizeof(float*) * n);
    gone = (int*)malloc(sizeof(int) * n);

    for (i = 0; i < n; i++)
    {
        city[i] = (float*)malloc(sizeof(float) * n);
        gone[i] = 0;

        for (j = 0; j < n; j++)
        {
            fscanf(in, "%f", &city[i][j]);
            printf("%.1f ", city[i][j]);
        }

        printf("\n");
    }

    fscanf(in, "%d%d", &start, &end);
    printf("%d %d\n", start, end);

    // 呼叫遞迴函式
    care(start - 1);

    // 釋放記憶體空間
    for (i = 0; i < n; i++) { free(city[i]); }
    free(city);
    free(gone);

    printf("%.5f\n", (1 - num));
    fprintf(out, "%.5f\n", (1 - num));

    system("pause");
}

void care(int now)
{
    int i;

    // 假如走到終點則加總機率後返回
    if (now + 1 == end)
    {
        num *= 1 - chance;
        return;
    }

    // 標記走過的城鎮
    gone[now] = 1;

    for (i = 0; i < n; i++)
    {
        // 跳過機率為零的路徑及走過的城鎮
        if (!city[now][i] || gone[i]){ continue; }

        // 呼叫遞迴計算機率
        chance *= city[now][i];
        care(i);
        chance /= city[now][i];
    }

    gone[now] = 0;
}

4月 11, 2008

【解題】最大矩形 - Area

@
台北市95學年度高中資訊學科能力競賽 - 最大矩形 (Area)


問題描述

  在一個 M x N 的區域內,散落了許多不同的障礙物,我們想要知道的是,在這個 M x N 的區域內,最大的矩形空地面積是多少?倘若我們用 0 與 1 表示這個區域內的空地狀況:0 代表這個子區域已被障礙物覆蓋,1 代表這個子區域仍為空地,我們假設每一個 0 或 1 所代表的子區域面積為 1,那麼在下面這個例子中(M = 4,N = 5),最大的矩形空地為陰影所覆蓋的區域,其面積為 8。

   0 0 1 1 0
   0 1 1 1 1
   0 1 1 1 1
   0 0 1 0 0


  在本題中,請依據輸入輸出的規定,針對輸入的地圖,輸出其最大的矩形空地面積。


輸入檔格式(C:\area\input.txt)

  輸入檔第一行有兩個整數,依序為 M 和 N, M ≦ 200, N ≦ 200;接下來的 M 行中,每一行有 N 個 0 或 1 的數字。這 N 個數字彼此間用一個空白隔開。


輸出檔格式 (C:\area\output.txt)

  請將最大矩形空地面積寫出至輸出檔。


輸入檔範例

4 5
0 0 1 1 0
0 1 1 1 1
0 1 1 1 1
0 0 1 0 0


輸出檔範例

8


解題思考

  題目要求我們求出"內部全是 1"最大矩形,我們就直觀的作吧。

  首先,使用程式在儲存輸入資料的二維陣列中,用兩層迴圈(i, j)去找一個空地(即變數值為 1)為起始點。接下來的問題,就是如何尋找、並判斷矩形的大小。

  在這裡,我在剛剛的兩層迴圈之中,再用了第三四層迴圈(x, y)。先由 x = 0 開始,得到從 i 開始最後一個連續空地的位置(y + j),並以 x + 1 * y + 1 (因為(x, y)座標都是以 0 開始)計算矩形大小。除此之外,還需要一個變數(方便說明,下面以 y' 表示),把這個位置記錄下來。

  當 x = 1 時,y 就不會超過剛剛所存變數的位置。假如到這個位置之前,找到最後一個連續的空地位置(即在 y < y' 時就遇到 0),則將 y' 更新為當前位置。並且,別忘了,在 x 遞增前,計算出這個矩形的大小。以此類推到 x = n 為最後一個連續空地。

  或許有點難懂,這裡我們以範例輸入為例,假設我們以(3, 0)(題目以向下為x軸正向,向右為y軸正向)作為抓取矩形的起點:



  上圖中的標上紅色的部份,是每一次計算矩形大小時抓取的範圍。標上綠色的部份,是停止抓取的分界。可以注意到,由於在第一張圖中的(0, 4)為0,因此在之後的(x, y)中,y都不會大於等於4。

  這麼做的目的是什麼呢?假設現在在第二張圖中,我們將(1, 4)也納入判斷矩形的範圍。由於(0, 4)之值為0,因此在以(3, 0)為起點時,y大於等於4的情況下都不可得出一個矩形的。這麼做的目的就在排除這已知的情況。

  只要這樣做下去,就能夠得出以某一點為起點的情況中,各種可能矩形面積大小。只要把每個能夠當成起點的部份(空地,即值為1)得出的面積大小做比較,自然就能夠得出最大面積了。


參考解答(C)

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int i, j, m, n, x, y, e, max = 0;
    int **area;

    // 宣告檔案串流
    FILE *in,*out=fopen("C:/area/output.txt","w+");

    if (!(in=fopen("C:/area/input.txt","r")))
    {
        printf("找不到 input.txt\n");
        system("pause");
        exit(1);
    }

    printf("讀入資料 . . .\n");
    fscanf(in, "%d %d", &m, &n);
    printf("%d %d\n", m, n);

    // 動態配置記憶體空間
    area = (int**)malloc(sizeof(int*) * m);

    for (i = 0; i < m; i++)
    {
        area[i] = (int*)malloc(sizeof(int) * n);

        for (j = 0; j < n; j++)
        {
            fscanf(in, "%d", &area[i][j]);
            printf("%d ", area[i][j]);
        }

        printf("\n");
    }

    fclose(in);

    for (i = 0; i < m; i++)
    {
        for (j = 0; j < n; j++)
        {
            // 若 (i, j) 為 1 時
            if (area[i][j])
            {
                e = 0;
                for (x = 0; x < m - i; x++)
                {
                    for (y = 0; y < n - j - e; y++)
                    {
                        if (!area[x + i][y + j])
                        {
                            // (n - j - e) 為矩形最大寬度
                            e = n - j - y;
                            break;
                        }

                        // 計算最大值
                        max = max > (x + 1) * (y + 1) ? max : (x + 1) * (y + 1);
                    }
                }
            }
        }
    }

    // 釋放記憶體空間
    for (i = 0; i < m; i++) { free(area[i]); }
    free(area);

    fprintf(out, "%d", max);
    fclose(out);

    printf("最大矩形面積為 %d\n", max);
    printf("輸出資料在 ouput.txt\n");

    system("pause");
}

4月 08, 2008

【解題】用餐地點 - Lunch

@
台北市95學年度高中資訊學科能力競賽 - 用餐地點 (Lunch)


問題描述

  越野競賽暑期訓練營近年來報名人數大增,因此訓練營把3公頃大的訓練場隔成 m x n 個相鄰的區域,每兩個相鄰(上、下、左、右)的區域之間都有教練駐守,因此除了用餐時間外,學員不得隨意跨區接受訓練。每天早上雖然營長會將所有人分配到不同的區域進行訓練,到了午餐時,為了避免學員來回奔波,因此會選定某一區搭設臨時帳棚並將所有學員集中到這一區來用餐,當午餐時間到來時,每一區的學員就會自行往用餐區移動。根據以往的經驗,平均來講,每 0.1 秒就可以有一人從自己所在區域移動到一個相鄰的區域(換句話說,每一秒可以有 10 人次轉換至相鄰區域)。為了使所有學員能儘速到達用餐區,請寫一個程式幫營長決定應該將午餐設在哪一區。

範例:
  以下圖為例,訓練場已分割成 3 x 5 個區域,其中第(1, 1)區有5個學員、第(3, 4)區有3個學員,而第(1, 3), (2, 1), (2, 3), (2, 5), (3, 2), (3, 3)及(3, 5)區都沒有任何學員。




輸入檔格式(C:\lunch\input.txt)

  測試檔案的第一行有兩個數字 m, n, (1 <= m, n <= 100)分別代表訓練場的區隔方式(如上圖所示),因此第 i, j 區的區域代號就是(i, j)。接下來的 m 行,每一行有 n 個整數,這 m 行中的第 i 行的第 j 個整數代表(i, j)區的學員數。每兩個整數之間都會有一個空白。


輸出檔格式 (C:\lunch\output.txt)

  請輸出能最快讓所有學員都能集中到達用餐的區域代號。輸出時,不需要輸出括號或逗點,只需要將代表該區的兩個整數輸出並以空白隔開。


輸入檔範例

3 5
5 2 0 2 2
0 2 0 2 0
1 0 0 3 0


輸出檔範例

1 2


解題思考

  首先分析題目的意思,我們可以知道,要求的結果是希望讓"所有人的移動區域數總和"為最小的最佳化題目。

  至於移動區域數的計算方式,假設現在某甲的位置在(a, b),則某甲移動到(i, j)的移動區域數就等於:|a - i| + |b - j|。

  接著,使用暴力法,計算每個區域的移動總格數,求出能得到最小值的地點,就是答案了。


參考解答(C)

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int main(void)
{
    int m, n, x, y, fx, fy, i, j, sum, a, b, min = INT_MAX;
    int** man;

    // 宣告檔案串流
    FILE *in, *out = fopen("C:/lunch/output.txt", "w+");

    if (!(in = fopen("C:/lunch/input.txt","r")))
    {
        printf("找不到 input.txt\n");
        system("pause");
        exit(1);
    }

    printf("讀入資料 . . .\n");
    fscanf(in, "%d %d", &m, &n);
    printf("%d %d\n", m, n);

    // 動態配置記憶體空間
    man = (int**)malloc(sizeof(int*) * m);
    for (i = 0; i < m; i++)
    {
        man[i] = (int*)malloc(sizeof(int) * n);

        for (j = 0; j < n; j++)
        {
            fscanf(in,"%d", &man[i][j]);
            printf("%d ", man[i][j]);
        }

        printf("\n");
    }

    fclose(in);

    for (a = 1; a <= m; a++)
    {
        for (b = 1; b <= n; b++)
        {
            // 設用餐地點為 (a, b)
            sum = 0;
            for (i = 0; i < m; i++)
            {
                for (j = 0; j < n; j++)
                {
                    // 求出 (i, j) 區域的人到達 (a, b) 區域的時間
                    x = (i + 1 - a) > 0 ? (i + 1 - a) : (a - i - 1);
                    y = (j + 1 - b) > 0 ? (j + 1 - b) : (b - j - 1);

                    sum += man[i][j] * (x + y);
                }
            }

            // 求出最小值
            if (min > sum)
            {
                min = sum;
                fx = a;
                fy = b;
            }
        }
    }

    // 釋放記憶體空間
    for (i = 0; i < m; i++) { free(man[i]); }
    free(man);

    fprintf(out, "%d %d\n", fx, fy);
    fclose(out);

    printf("吃飯地點為 (%d, %d)\n", fx, fy);
    printf("輸出資料在 ouput.txt\n");

    system("pause");
}

4月 07, 2008

【解題】井字遊戲 - TTT

@
台北市95學年度高中資訊學科能力競賽 - 井字遊戲 (TTT)


問題描述

  井字遊戲是三、四歲小孩就會玩的棋賽。在一個 3 列(row) x 3 行(column)的空格中,參與遊戲的兩人一個只能填入 O,另一個只能填入 X,兩人從頭到尾輪流填入自己的符號,誰先將自己的符號在垂直、水平或斜角方向連成 3 個,誰就勝利。在此假設遊戲一開始都是填入 O 的人先填。

  請寫一個程式,來判斷給定任一未完成的棋局,其最後結束時(O 贏、X 贏或平手)共有幾種不同的棋盤面,而其中 O 贏的棋盤面、X 贏的棋盤面、和局的棋盤面各有幾種。

  例如,下圖中,最左邊的棋局,其結束時的棋盤面會有右邊四種可能。第一種 O 贏,第二種平手,第三、四種都是 X 贏,因此輸出(4, 1, 2, 1)。




輸入檔格式(C:\ttt\input.txt)

  測試檔案中共有一行,以九個連續符號表示未完成的棋局,前三個符號代表該棋局第一列的三個格子的狀態,第四到第六個符號代表該棋局第二列的三個格子的狀態,第七到第九個符號代表該棋局第三列的三個格子的狀態。這些符號除了「O」與「X」外,以「-」來代表尚未被填入的格子。輸入檔中不會有不合理的棋局出現。


輸出檔格式 (C:\ttt\output.txt)

  依序輸出該棋局最後可能出現的不同棋盤面數、O 贏的棋盤面數,X 贏的棋盤面數,以及平手的棋盤面數,此四個數字以空白隔開。


輸入檔範例1

OX--X-XOO


輸出檔範例1

4 1 2 1


輸入檔範例2

O-OXO-X--


輸出檔範例2

11 9 1 1


輸入檔範例3

O-OXO-X-X


輸出檔範例3

4 2 1 1


解題思考

  對於這一題,我選擇使用遞迴函式來模擬雙方下棋。

  首先,我們注意到一個不太明顯的小地方。棋盤的空白處(即未下棋的地方)數量,除了可以來判斷棋盤是否已下滿之外,還可以用它代表現在下棋的人是 O 或是 X。由於是 O 先下,所以剩餘空白處的數量只是奇數就是 O 下,相反的,是偶數則為 X 下。

  我們根據以上提到的,判斷由誰下棋。並藉由迴圈來模擬下在各種地方的情況。並且,在每下完一步都檢查是否分出勝負。

  單是這樣還不行,因為即使下的順序不同,最後可能還會出現棋局重複的情形。因此,我們還需要儲存已出現過的棋局。你可以使用一個二維字元陣列。

  不過,由於我不想耗費一個陣列存一個棋局(就是棋盤的每一格由一個陣列元素儲存),我將棋局轉換成一個九位數的整數。第一位代表第 1 格,第二位代表第 2 格,以此類推。這樣一個棋局就只需要一個整數變數就能代表了。當然,僅供參考,有人有更好的方法也可以提出。


參考解答(C)

#include <stdio.h>
#include <stdlib.h>

void play(void);
int search(void);

int num = 0, win[2] = {0, 0}, top = 1, st[500];
char t[9];

int main(void)
{
    int i, sum;

    // 宣告檔案串流
    FILE *in, *out = fopen("C:/ttt/output.txt", "w+");
    if (!(in = fopen("C:/ttt/input.txt", "r")))
    {
        printf("找不到 input.txt\n");
        system("pause");
        exit(1);
    }

    printf("讀入資料 . . .\n");
    for (i = 0; i < 9; i++)
    {
        t[i] = fgetc(in);
        printf("%c", t[i]);
        if (t[i] == '-') { num += 1; }
    }

    fclose(in);

    printf("\n---------\n");

    // 呼叫遞迴處理棋局
    play();
    sum = win[0] + win[1] + win[2];

    // 將結果印在輸出檔
    fprintf(out, "%d %d %d %d", sum, win[1], win[0], win[2]);
    fclose(out);

    // 將結果標準輸出
    printf("局數: %d\nO 勝: %d\nX 勝: %d\n和局: %d\n", sum, win[1], win[0], win[2]);
    printf("---------\n輸出資料在 ouput.txt\n");

    system("pause");
}

void play(void)
{
    int i;

    // 判別是否分出勝負
    if ((t[4] != '-') && (((t[0] == t[4]) && (t[4] == t[8]))
        || ((t[6] == t[4]) && (t[4] == t[2]))))
    {
        if (search()) { return; }

        win[(num + 1) % 2] += 1;

        return;
    }

    for (i = 0; i < 3; i++)
    {
        if (((t[i] != '-') && (t[i] == t[i + 3]) && (t[i] == t[i + 6]))
            || ((t[3 * i] != '-') && (t[3 * i] == t[3 * i + 1])
            && (t[3 * i] == t[3 * i + 2])))
        {
            if (search()) { return; }

            win[(num + 1) % 2] += 1;

            return;
        }
}

    // 判別是否和局
    if (num == 0)
    {
        if(search()) { return; }

        win[2] += 1;

        return;
    }

    // 使用窮舉法實現每種可能
    for (i = 0; i < 9; i++)
    {
        if (t[i] == '-')
        {
            t[i] = num % 2 ? 'O' : 'X';
            num--;
            play();
            num++;
            t[i] = '-';
        }
    }
}

int search(void)
{
    int i, temp = 0;

    // 將棋局轉換存入變數
    for (i = 0; i < 9; i++)
    {
        if (t[i] == 'O') { temp = temp * 10 + 1; }
        else if (t[i] == 'X') { temp *= 10; }
        else { temp = temp * 10 + 2; }
    }

    for (i = 0; i < top; i++)
    {
        // 查詢棋局是否重複
        if (temp == st[i]) { return 1; }
    }

    // 將目前棋局記錄下來
    st[top] = temp;
    top++;

    return 0;
}

4月 04, 2008

【作品】猜數字 - Guess (PHP ver.)

@
  這個猜數字,跟我之前發表用C寫的猜數字並無二致,規則當然也相同。只是由於當時正在學PHP的關係,老師又正好看到我寫出來的猜數字,便要我寫一個PHP版本的出來。


程式說明

  我們沒什麼好說的,有問題看之前那篇。


程式連結

  

4月 03, 2008

【作品】小小樂透彩 - Lottery

@
  話說台灣人的天性就是!根據甄虎滥專家所言,這是由於當初華人不顧台灣海峽危險,冒險犯難、賭命的精神,才造就了這種特殊的性格。因此,樂透彩自然是台灣人不可少的"休閒活動"!

  扯太遠了。這支程式是當初學PHP時,Tory PHP講義裡的進階題(進階嗎)。因為感覺還滿有趣的,就把它做出來了。


程式說明

  規則比照實際樂透彩玩法(只是不提供電腦選號)。

  什麼?你不知道樂透怎麼玩?你還算台灣人嗎(毆)!好啦,讓我們大致簡介一下規則。

  首先,共有1~42號,玩家先選擇6個號碼之後按下Play即可。得什麼獎就根據你的號碼與電腦開出的號碼相同的數量。至於中多少個,得什麼獎,網頁裡就有說明。


程式連結

  

4月 02, 2008

【作品】奧塞羅棋 - Othello

@
  以前其實我不太會玩Othello,連跟電子辭典那種超級弱的電腦都可以下輸。之後,在我老爸的指點之下,才發現原來我犯的是標準的新手病,只挑能吃最多子的地方下。為了示範該怎麼下,老爸就用當時Windows XP內建的網際網路黑白棋,來實際下一次給我看。

  之後,只要晚上我們兩個有空(應該說是等我老爸有空,當時我閒的很),就會坐在電腦前邊跟網路上的對手下,邊研討兩個人的"戰術"。

  雖然到最後,只剩下我仍會在無聊時,打開"網際網路黑白棋"找對手下棋。不過也在跟許多高手的對決中學了幾招。

  擁有這種經驗,Othello理所當然是我最喜愛的棋奕遊戲。又因為看了學長的大學作業(他做的是五子棋),覺得相當有趣。想說不如模仿它,改做出一個Othello棋。正好那時也想測試看看Winsock的使用,所以這支程式就這麼做出來了。


程式說明

  奧賽羅棋,又稱黑白棋或是蘋果棋。規則是兩個自己的子連成一線,即可把連線中對方的子吃掉。直到雙方都無法繼續下棋時,棋子數量多的人就贏了。

  這裡我們不花太多時間解釋規則,對規則還是不了解的可以查查Wikipedia的條目。

  這個程式能夠單機雙人,兩人用同一台電腦輪流下、連線雙人,藉由輸入IP讓一方連到另一方的電腦兩人對戰。除此之外,還有一個單機單人,就是跟我寫的一個超簡易的AI對戰。不過這個AI一點都不強就是了。






程式下載

  


其他

  原本想作出強一點的AI,或是製作視窗版的程式,現在都遲遲沒動作。我的懶的確該檢討了.....。

4月 01, 2008

【解題】函數計算 - Comp

@
台北市95學年度高中資訊學科能力競賽 - 函數計算 (Comp)


問題描述

  小明的數學老師出了一題函數計算的家庭作業,由於筆算的過程非常複雜,請你寫一個程式協助小明以及班上的其他同學進行驗算,以確定計算的結果是正確的。老師所出的題目是這樣的:給定一個整數 x,請求出函數 f 的值為何?




輸入格式(標準輸入)

  直接從標準輸入(鍵盤)輸入一整數 x,-300 < x < 300。


輸出格式(標準輸出)

  請將計算過後函數 f 的值直接輸出至標準輸出(螢幕)。


輸入範例1

3


輸出範例1

-1


輸入範例2

-2


輸出範例2

-4


輸入範例3

-21


輸出範例3

-1307


解題思考

  乍看之下題目還滿簡單的,因此就直接照著題目的定義做吧。

  然後接下來你會發現:大部分的測試資料程式都跑不動!這算是題目設計的小陷阱,假如就這樣照著題意實現,就會因為遞迴的呼叫,導致堆疊過多以致於溢位。

  因此,在撰寫程式前,我們得先對題目做簡易的分析。

  首先看到 f(x),很明顯可以發現,其麻煩在於要比較 x 與 h(x) 的大小。因為 g(x) 的計算相對容易,在這裡,我選擇要簡化的是 x > h(x) 的情況。

  又可以發現若且惟若 -3 < x < 2 或 x > 4 時,x > h(x) (可以自己手動計算看看),因此我們從這個範圍的數字開始分析(我們先手動算出 f(-1) = 1, f(4) = -1):

   f(0) = f(-1) - h(1) = 1 - h(1)
   f(1) = f(0) - h(1) = f(-1) - h(0) - h(1) = 1 - h(0) - h(1)


  由上式整理可得,在 -3 < x < 2 時:

   f(x) = 1 - (h(0) + h(1) + ... + h(x))

   f(5) = f(4) - h(5) = -1 - h(5)
   f(6) = f(5) - h(6) = f(4) - h(5) - h(6) = -1 - h(5) - h(6)
   f(7) = f(6) - h(7) = f(5) - h(6) - h(7) = f(4) - h(5) - h(6) - h(7) = -1 - h(5) - h(6) - h(7)


  同樣整理可得,在 x > 4 時:

   f(x) = -1 - (h(5) + h(6) + ... + h(x))

  藉由這樣展開,就省去許多遞迴呼叫 f(x - 1) 的狀況(尤其在 x 相當大的情況)。(不過在這裡我可能還做複雜了,假如有誰有更好的方法,歡迎提出。)

  接著,我們看到 h(y) 在 y 大於等於 2 的情況:

   h(2) = 2
   h(3) = 5
   h(4) = 5
   h(5) = -1
   h(6) = -1
   h(7) = 2

   h(8) = 2
   h(9) = 5
   h(10) = 5
   h(11) = -1
   h(12) = -1
   h(13) = 2


  這裡,我們可以發現,h(y) 每六個數字為一個循環週期,因此,我們只要根據 y 除以 6 的餘數,就能夠判斷 h(y) 的結果了。

  最後,g(z) 就照著題目的定義實現即可。


參考解答(C)

#include <stdio.h>
#include <stdlib.h>

int f(int), h(int), g(int);

int main(void)
{
    int x;

    printf("請輸入一個整數X:");
    scanf("%d", &x);
    printf("函數計算結果:%d\n", f(x));

    system("pause");
}

// f(x)的函數
int f(int x)
{
    int i, ret;
    if (x > h(x))
    {
        i = x < 5 ? 0 : 5;
        ret = x < 5 ? 1 : -1;

        for (; i <= x; i++)
        {
            ret -= h(i);
        }

        return ret;
    }
    else if (x < h(x))
    {
        return f(g(x)) - g(x);
    }
    else
    {
        return 1;
    }
}

// h(y)的函數
int h(int y)
{
    if (y < 2)
    {
        return -1;
    }
    else
    {
        switch(y % 6)
        {
            case 2:
            case 5:
                return 2;

            case 3:
            case 4:
                return 5;

            default:
                return -1;
        }
    }
}

// g(z)的函數
int g(int z)
{
    if(z <= 2)
    {
        return z * z - 1;
    }
    else
    {
        return 2;
    }
}