SCIST 2023

12/24

建表

核心概念

切分「資料」與「邏輯」

主要用途

  • 讓 code 變得簡潔、易於修改
  • 降低出 bug 的風險

例題:Kattis trik

有三個杯子杯底朝上排成一橫排(左、中、右),左杯放一顆球,給一連串交換指令後,求球位在哪個杯子?

  • A:交換左、中兩杯
  • B:交換中、右兩杯
  • C:交換左、右兩杯

直觀做法

bool left = true;
bool mid = false, right = false;

for (i=0; i<cmd.size(); i++)
{
	if (cmd[i] == 'A')
    {
    	swap(left, mid);
    }
    else if (cmd[i] == 'B')
    {
    	swap(mid, right);
    }
    else
    {
    	swap(left, right);
    }
}

問題點

  • 缺乏彈性,難以擴充
  • 邏輯和資料混在一起

試想杯子變成 10 個的話,如何修改?

光想就頭大

  • 需要 10 個不同名變數
  • 總共需要 (10*9)/2 = 45 種交換方式
  • 最後判別球在哪也要 10 個 if

建表拆分資料與邏輯

const int CUP_N = 3;
const int TARGET[CUP_N][2] = {{0, 1}, {1, 2}, {0, 2}};

bool cup[CUP_N] = {true};

for (i=0; i<cmd.size(); i++)
{
	int t = cmd[i] - 'A';
    swap(cup[ TARGET[t][0] ], cup[ TARGET[t][1] ]);
}

給日期,輸出星座

超.級.麻.煩

這類問題還有個特色是極難 debug

應對一:形式轉換

月日太麻煩,全轉成天數方便比對

月份天數不同,老實轉不如隨便轉

應對一:形式轉換

X 月 Y 日 → X * 100 + Y

應對二:查表

寫 12 個 if 轉換月份英文 → 數字?

應對二:查表

const string MONTH[] = {"", "Jan", "Feb", "Mar", "Apr", 
							"May", "Jun", "Jul", "Aug", 
                            "Sep", "Oct", "Nov", "Dec"};

int str_to_month(const string& s)
{
	for (int i=1; i<=12; i++)
    {
    	if (s == MONTH[i])
        {
        	return i;
        }
    }
    return -1;
}
int day;
string mon_str;
cin >> day >> mon_str;
int res = str_to_month(mon_str) * 100 + day;

應對二:查表

  • 切分資料與邏輯
  • 邏輯變得簡潔不易錯
  • 資料變得容易檢查與整理

應對三:再次查表

對十二星座各寫 if?

應對三:再次查表

const int DAY[] = {120, 219, 320, 420, 
				   520, 621, 722, 822, 
                   921, 1022, 1122, 1221, 9999};
const string NAM[] = {"Capricorn", "Aquarius", "Pisces", "Aries", 
					  "Taurus", "Gemini", "Cancer", "Leo", 
                      "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn"};
int day;
string mon_str;
cin >> day >> mon_str;
int res = str_to_month(mon_str) * 100 + day;

int ans;
for (ans=0; res>DAY[ans]; ans++);
cout << ans << "\n";

建表

就是如此優雅簡潔

思考方向

先想「有什麼表方便」再想「如何去建」

給一棋盤盤面,問是否恰有 9 隻互不攻擊的西洋棋騎士?

暴力解

bool gg = false;
int knight_num = 0;
// 窮舉所有座標
for (i=0; i<5; i++)
{
	for (j=0; j<5; j++)
    {
    	// 如果是騎士
    	if (board[i][j] == 'k')
        {
        	knight_num++;
            // 檢查攻擊範圍
            if (i-2 >= 0 && j-1 >= 0 && board[i-2][j-1] == 'k')
            {
            	gg = true;
            }
            if (i-2 >= 0 && j+1 < 5 && board[i-2][j+1] == 'k')
            {
            	gg = true;
            }
            if (i-1 >= 0 && j-2 >= 0 && board[i-1][j-2] == 'k')
            {
            	gg = true;
            }
            if (i-1 >= 0 && j+2 < 5 && board[i-1][j+2] == 'k')
            {
            	gg = true;
            }
            if (i+1 < 5 && j-2 >= 0 && board[i+1][j-2] == 'k')
            {
            	gg = true;
            }
            if (i+1 < 5 && j+2 < 5 && board[i+1][j+2] == 'k')
            {
            	gg = true;
            }
            if (i+2 < 5 && j-1 >= 0 && board[i+2][j-1] == 'k')
            {
            	gg = true;
            }
            if (i+2 < 5 && j+1 < 5 && board[i+2][j+1] == 'k')
            {
            	gg = true;
            }
        }
    }
}
if (knight_num != 9)
{
	gg = true;
}

超.級.麻.煩

容易錯,不容易發現,不好救

何不建表?

不規則怎麼辦?

建表解決

不規則的東西,扔進陣列就會乖了

建表解決

const int DX[] = {2, 2, 1, 1, -1, -1, -2, -2};
const int DY[] = {1, -1, 2, -2, 2, -2, 1, -1};

bool in_range(int x, int y)
{
	return x >= 0 && x < 5 && y >= 0 && y < 5;
}
bool gg = false;
int knight_num = 0;
// 窮舉所有座標
for (i=0; i<5; i++)
{
	for (j=0; j<5; j++)
    {
    	// 如果是騎士
    	if (board[i][j] == 'k')
        {
        	knight_num++;
            // 檢查攻擊範圍
            for (k=0; k<8; k++)
            {
            	int tx = i + DX[k];
                int ty = j + DY[k];
                if (in_range(tx, ty) && board[tx][ty] == 'k')
                {
                	gg = true;
                }
            }
        }
    }
}
if (knight_num != 9)
{
	gg = true;
}

這要寫錯都難了吧

二維表格(矩陣)

用來對付像「A 和 B 的關係」用

例如屬性相剋表

例題:Kattis majstor

和 N 朋友玩 M 回猜拳,每回合出一種拳,和所有朋友個別判定勝負來計算得分。求按題目給定的出拳順序的得分、以及預先知道朋友出拳時可能的最高得分。

 

每次猜贏得 2 分,平手得 1 分,輸掉得 0 分。

建立二維表計算得分

主角\朋友 S P R
S 1 2 0
P 0 1 2
R 2 0 1

宣告常數代替直接寫死數字

const int WIN = 2;
const int TIE = 1;
const int LOSE = 0;

const int SCORE[3][3] = {
	{TIE, WIN, LOSE},  // S -> SPR
	{LOSE, TIE, WIN},  // P -> SPR
	{WIN, LOSE, TIE},  // R -> SPR
};

快樂建表法《二段建表》

手動建會有點痛扣時用

例題:UVa 10082

給一字串為鍵盤輸入時,全往右按偏 1 格的結果,求它原本想輸入什麼?

如果建好表的話

for (i=0; i<s.size(); i++)
{
	s = tbl[ s[i] ];
}

超級快樂

建表時

tbl['W'] = 'Q';
tbl['E'] = 'W';
tbl['R'] = 'E';
tbl['T'] = 'R';
// ...... 將近 50 組

異常痛扣

二段式建表

// 滑過鍵盤四趟即可,不用 10 秒
const string KEY = "`1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./";

char tbl[128];
for (i=1; i<KEY.size(); i++)
{
	tbl[ KEY[i] ] = KEY[i-1];
}
tbl[' '] = ' ';

建表就是如此快樂

快樂建表法《寫扣建表》

如果二段也很痛扣,何不讓程式代勞

按下表計算列印墨水費

WTF????????

奧義:寫扣偷懶

char c;
int d;
while (cin >> c >> d)
{
	cout << "tbl['" << c << "'] = " << d << ";\n";
}

建表就是如此快樂

你可以開始嘲笑旁邊寫幾十個 if 的勤勞朋友了

記得微調特殊字元

窮舉

核心概念:窮舉

直接求解困難時

  • 找出解可能存在的範圍
  • 對範圍內每個東西,檢查它是不是解

例題:ZJ f312

求解太難

那你有試過窮舉嗎?

窮舉法

已知 X1 可能範圍為 0 到 n

窮舉 X1 所有可能,每種得到 X2 = n - X1

代入求收益 Y1+Y2,取最大者

窮舉答案

找出所有可能情形,每一組代回去比對

例題:UVa 608

給 12 硬幣,已知有 1 為假,和真硬幣重量不同,但可能較輕、可能較重;

給多次秤重時天平兩邊硬幣與結果,求假硬幣為何、以及是較真硬幣輕或重。

直接求解?

人類習慣邏輯推論,方向不對還能想新的

電腦不行,一開始的招式不完整就是錯

可能的答案有幾種?

  • A 輕
  • A 重
  • B 輕
  • B 重
  • C 輕

共計 12 x 2 = 24 種

假設並代入

設 A 為輕,則 W[A] = 1、W[其它] = 2

檢查所有秤重結果是否矛盾

左右兩側各自查表相加,檢查大小,簡單吧?

檢查矛盾數

  • 成立數 > 1:多重解
  • 成立數 == 1:單一解
  • 成立數 == 0:無解

馬上變成 EZ Game

多維窮舉

如果所有情況會是數對等多參數的東東

例題:UVa 11059

求區間 [l, r] 使區間內所有元素乘起來最大

考慮負數

負數的存在使得區間並非越長越好,怎辦?

不如窮舉所有區間

N 最大 18,此時區間數 (18 * 17) / 2 = 153

窮舉所有區間

long long best = -1e18;
for (i=0; i<n; i++)
{
	long long t = 1;
	for (j=i; j<n; j++)
    {
    	t *= ary[i];
        best = max(best, t);
    }
}

實作速度

如何提升速度?

練打字?練短碼?

基本沒用

那怎辦?

原則上,速度 = 穩定度

一次 AC 最快

AC 過程

  • 題敘閱讀理解
  • 流程規劃
  • 實作
  • 測試除錯

哪一步能偷時間?

閱讀理解?

如果你理解錯,寫對時就是不符題意

那你再強都沒用

流程規劃?

不先規劃,邊寫邊想容易前後矛盾對不起來

實作?

能節省的有限,大多有害

測試除錯?

時間最沒上限、最難壓但也最沒副作用

分段測試

流程先拆步驟,定義規範、個別測試

完成數個小任務,比單一大任務簡單

定義每步解決的問題

決定每步輸入什麼、計算出什麼、代表什麼意義

定義如何對接

定義輸出輸入的規格,例如存在哪些變數、index 開頭結尾…等

個別測試

每步各自測試,bug 相對好找、究責時相對容易

證錯,而非證對

測資必須以「證明這程式是錯的」出發,出到無能為力為止

可以想像這份是你討厭的人寫的,有助於找出盲點或破綻

競賽策略

競賽不只比技術與知識

其它像是身心狀態的管理與調節、資源分配…

賽前適度放鬆,不要生病

其實這異常重要

閱讀過所有題目

不論是否依難度排序

練習一次 AC

以「一次沒過,這題就 0 分」的心態面對

練習估計所需時間

並且計時,習慣時間壓力,了解自己做什麼需要多久,和實際費時比對、修正誤差

每題估計耗時與得分

從 CP 值最高的做,估時越準越有幫助

適度跳脫思考

一定時間內進展過小,就先想別題或喝個水等

跳脫目前思考後再重新來過

敘述解法

嘗試說明有助整理和找出缺漏

出 bug 時說明想法有助找出問題和破綻

不想給別人看 or 禁止交談時,打字也行

測資複製貼上

測試佔非常多時間,建議複製貼上

題目純紙本時,可打在 IDE 上,善用全選

必要時寫成多重輸入來應對多測資

隨機選題訓練

(建議一定程度以上後)

練習特定題型題單,會有太多事前資訊

難以訓練題型判讀與面對未知時如何找線索

模擬

照題目規則模擬過程

核心在管理狀態的變化

用一組整數儲存現況

例如現在回合、輪到玩家幾、目前階段…

非數字者編碼為整數

例如勝敗、死活、站立坐下、高興難過、…

一對一映射至整數,宣告常數增加可讀性

記錄變動

同回合的行動無法「同時進行」

可記錄變動量延時套用

例題:ZJ f313

SCIST 231224

By sa072686

SCIST 231224

  • 657