北京大學C語言學習第10天

《算法基礎與在線實踐》
所有例題,根據題目名稱在
http://openjudge.cn
“百練”組進行搜索即可提交
枚 舉
 基於逐個嘗試答案的一種問題求解策略
 例如: 求小於N的最大素數
– 找不到一個數學公式, 使得根據N就可以計算出這個素數
– N-1是素數嗎? N-2是素數嗎? ……
判斷N-i是否是素數的問題
轉化爲求小於N的全部素數(可以用篩法)
4
例題:完美立方
信息科學技術學院
內蒙古阿斯哈圖石林
例題1:完美立方
 形如a
3= b3 + c3 + d3的等式被稱爲完美立方等式。例如
123= 63 + 83 + 103 。編寫一個程序,對任給的正整數N
(N≤100),尋找所有的四元組(a, b, c, d),使得a
3 = b3 +
c
3 + d3,其中a,b,c,d 大於 1, 小於等於N,且b<=c<=d。
 輸入
一個正整數N (N≤100)。
 輸出
每行輸出一個完美立方。輸出格式爲:
Cube = a, Triple = (b,c,d)
其中a,b,c,d所在位置分別用實際求出四元組值代入。
6
完美立方
請按照a的值,從小到大依次輸出。當兩個完美立方
等式中a的值相同,則b值小的優先輸出、仍相同
則c值小的優先輸出、再相同則d值小的先輸出。
 樣例輸入
24
7
完美立方
 樣例輸出
Cube = 6, Triple = (3,4,5)
Cube = 12, Triple = (6,8,10)
Cube = 18, Triple = (2,12,16)
Cube = 18, Triple = (9,12,15)
Cube = 19, Triple = (3,10,18)
Cube = 20, Triple = (7,14,17)
Cube = 24, Triple = (12,16,20)
8
完美立方
 解題思路
四重循環枚舉a,b,c,d ,a在最外層,d在最裏層,每一層
都是從小到大枚舉,
a枚舉範圍[2,N]
9
完美立方
 解題思路
四重循環枚舉a,b,c,d ,a在最外層,d在最裏層,每一層
都是從小到大枚舉,
a枚舉範圍[2,N]
b範圍 [2,a-1]
10
完美立方
 解題思路
四重循環枚舉a,b,c,d ,a在最外層,d在最裏層,每一層
都是從小到大枚舉,
a枚舉範圍[2,N]
b範圍 [2,a-1]
c範圍 [b,a-1]
d範圍 [c,a-1]

#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int N;
scanf("%d",&N); 
for(int a = 2; a <= N; ++a) 
for(int b = 2; b < a; ++b)
for(int c = b; c < a; ++c)
for(int d = c; d < a; ++d)
if( a*a*a == b*b*b + c*c*c +d*d*d) 
printf("Cube = %d, Triple = (%d,%d,%d)\n", a, b, c, 
d);
return 0;
}

完美立方
例題:生理週期
 人有體力、情商、智商的高峯日子,它們分別每隔
23天、28天和33天出現一次。對於每個人,我們想
知道何時三個高峯落在同一天。給定三個高峯出現
的日子p,e和i(不一定是第一次高峯出現的日子),
再給定另一個指定的日子d,你的任務是輸出日子d
之後,下一次三個高峯落在同一天的日子(用距離d
的天數表示)。例如:給定日子爲10,下次出現三
個高峯同一天的日子是12,則輸出2。
14
生理週期
 輸入
輸入四個整數:p, e, i和d。 p, e, i分別表示體力、情感和智力
高峯出現的日子。d是給定的日子,可能小於p, e或 i。所有給
定日子是非負的並且小於或等於365,所求的日子小於或等於
21252。
 輸出
從給定日子起,下一次三個高峯同一天的日子(距離給定日子
的天數)。
15
生理週期  輸入樣例
0 0 0 0
0 0 0 100
5 20 34 325
4 5 6 7
283 102 23 320
203 301 203 40 -1 -1 -1 -1
16
生理週期
 輸出樣例
Case 1: the next triple peak occurs in 21252 days.
Case 2: the next triple peak occurs in 21152 days.
Case 3: the next triple peak occurs in 19575 days.
Case 4: the next triple peak occurs in 16994 days.
Case 5: the next triple peak occurs in 8910 days.
Case 6: the next triple peak occurs in 10789 days.
17
生理週期
 解題思路
• 從d+1天開始,一直試到第21252 天,對其中每個日期k,看
是否滿足
(k – p)%23 == 0 && (k – e)%28 == 0 &&
(k-i)%33 == 0
• 如何試得更快?
18
生理週期
 解題思路
• 從d+1天開始,一直試到第21252 天,對其中每個日期k,看
是否滿足
(k – p)%23 == 0 && (k – e)%28 == 0 &&
(k-i)%33 == 0
• 如何試得更快?
跳着試!
19

#include <iostream>
#include <cstdio>
using namespace std;
#define N 21252
int main(){
int p,e,i,d,caseNo = 0;
while( cin >> p >> e >>i >>d && p!= -1) {
++ caseNo;
int k;
for(k = d+1; (k-p)%23; ++k);
for(; (k-e)%28; k+= 23);
for(; (k-i)%33; k+= 23*28);
cout << "Case " << caseNo << 
": the next triple peak occurs in " << k-d << " days." << endl;
}
return 0;
}

20
例題:假幣問題
有12枚硬幣。其中有11枚真幣和1枚假幣。假幣和真
幣重量不同,但不知道假幣比真幣輕還是重。現在,
用一架天平稱了這些幣三次,告訴你稱的結果,請你
找出假幣並且確定假幣是輕是重(數據保證一定能找
出來)。
 輸入
第一行是測試數據組數。
每組數據有三行,每行表示一次稱量的結果。銀幣標號爲
A-L。每次稱量的結果用三個以空格隔開的字符串表示:
天平左邊放置的硬幣 天平右邊放置的硬幣 平衡狀態。其
中平衡狀態用up'',down’’, 或 ``even’'表示, 分別爲右
端高、右端低和平衡。天平左右的硬幣數總是相等的。
 輸出
輸出哪一個標號的銀幣是假幣,並說明它比真幣輕還是重。
例題:假幣問題
 輸入樣例
1
ABCD EFGH even
ABCI EFJK up
ABIJ EFGH even
 輸出樣例
K is the counterfeit coin and it is light.
例題:假幣問題
 解題思路
對於每一枚硬幣先假設它是輕的,看這樣是否符合稱
量結果。如果符合,問題即解決。如果不符合,就假
設它是重的,看是否符合稱量結果。把所有硬幣都試
一遍,一定能找到特殊硬幣
例題:假幣問題

#include <iostream>
#include <cstring>
using namespace std;
char Left[3][7]; //天平左邊硬幣
char Right[3][7]; //天平右邊硬幣
char result[3][7]; //結果
bool IsFake(char c,bool light) ;
//light 爲真表示假設假幣爲輕,否則表示假設假幣爲重
int main() {
int t;
cin >> t;
while(t--) {
for(int i = 0;i < 3; ++i) cin >> Left[i] >> Right[i] >> result[i];
for(char c='A'; c<='L';c++) {
if( IsFake(c,true) ){
cout << c << " is the counterfeit coin and it is light.\n";
break;
}
else if( IsFake(c,false) ){
cout << c << " is the counterfeit coin and it is heavy.\n";
break;
}
}
}
return 0; }

bool IsFake(char c,bool light) 
//light 爲真表示假設假幣爲輕,否則表示假設假幣爲重
{
for(int i = 0;i < 3; ++i) {
char * pLeft,*pRight; //指向天平兩邊的字符串
if(light) {
pLeft = Left[i];
pRight = Right[i];
}
else {//如果假設假幣是重的,則把稱量結果左右對換
pLeft = Right[i];
pRight = Left[i];
}
switch(result[i][0]) { //天平右邊的情況
case 'u':
if ( strchr(pRight,c) == NULL)
return false;
break;
case 'e':
if( strchr(pLeft,c) || strchr(pRight,c))
return false;
break;
case 'd':
if ( strchr(pLeft,c) == NULL)
return false;
break;
}
}
return true;
}

例題:熄燈問題
– 有一個由按鈕組成的矩陣, 其中每行有6個按鈕, 共5行
– 每個按鈕的位置上有一盞燈
– 當按下一個按鈕後, 該按鈕以及周圍位置(上邊, 下邊, 左
邊, 右邊)的燈都會改變狀態
31
例題4:熄燈問題
– 如果燈原來是點亮的, 就會被熄滅
– 如果燈原來是熄滅的, 則會被點亮
• 在矩陣角上的按鈕改變3盞燈的狀態
• 在矩陣邊上的按鈕改變4盞燈的狀態
• 其他的按鈕改變5盞燈的狀態
32
熄燈問題
 與一盞燈毗鄰的多個按鈕被按下時,一個操作會抵消另一次操
作的結果
 給定矩陣中每盞燈的初始狀態,求一種按按鈕方案,使得所有
的燈都熄滅
熄燈問題
 輸入:
– 第一行是一個正整數N, 表示需要解決的案例數
– 每個案例由5行組成, 每一行包括6個數字
– 這些數字以空格隔開, 可以是0或1
– 0 表示燈的初始狀態是熄滅的
– 1 表示燈的初始狀態是點亮的
34
熄燈問題
 輸出:
– 對每個案例, 首先輸出一行,
輸出字符串 “PUZZLE #m”, 其中m是該案例的序

– 接着按照該案例的輸入格式輸出5行
• 1 表示需要把對應的按鈕按下
• 0 表示不需要按對應的按鈕
• 每個數字以一個空格隔開35
熄燈問題
 樣例輸入
2
0 1 1 0 1 0
1 0 0 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
0 1 1 1 0 0
0 0 1 0 1 0
1 0 1 0 1 1
0 0 1 0 1 1
1 0 1 1 0 0
0 1 0 1 0 0
 樣例輸出
PUZZLE #1
1 0 1 0 0 1
1 1 0 1 0 1
0 0 1 0 1 1
1 0 0 1 0 0
0 1 0 0 0 0
PUZZLE #2
1 0 0 1 1 1
1 1 0 0 0 0
0 0 0 1 0 0
1 1 0 1 0 1
1 0 1 1 0 1 36
熄燈問題
解題分析
• 第2次按下同一個按鈕時,
將抵消第1次按下時所產生的結果
37
解題分析
• 第2次按下同一個按鈕時,
將抵消第1次按下時所產生的結果
每個按鈕最多只需要按下一次
38
解題分析
• 第2次按下同一個按鈕時,
將抵消第1次按下時所產生的結果
每個按鈕最多只需要按下一次
• 各個按鈕被按下的順序對最終的結果沒有影響
39
解題分析
• 第2次按下同一個按鈕時,
將抵消第1次按下時所產生的結果
每個按鈕最多只需要按下一次
• 各個按鈕被按下的順序對最終的結果沒有影響
• 對第1行中每盞點亮的燈, 按下第2行對應的按鈕, 就可以
熄滅第1行的全部燈
• 如此重複下去, 可以熄滅第1, 2, 3, 4行的全部燈
40
解題分析
• 第一想法: 枚舉所有可能的按鈕(開關)狀態, 對每個狀態計
算一下最後燈的情況, 看是否都熄滅
– 每個按鈕有兩種狀態(按下或不按下)
– 一共有30個開關, 那麼狀態數是2
30
, 太多, 會超時
41
解題分析
• 第一想法: 枚舉所有可能的按鈕(開關)狀態, 對每個狀態計
算一下最後燈的情況, 看是否都熄滅
– 每個按鈕有兩種狀態(按下或不按下)
– 一共有30個開關, 那麼狀態數是2
30
, 太多, 會超時
• 如何減少枚舉的狀態數目呢?
基本思路: 如果存在某個局部, 一旦這個局部的狀態被確定,
那麼剩餘其他部分的狀態只能是確定的一種, 或者不多的n
種, 那麼就只需枚舉這個局部的狀態即可 42
解題分析
• 本題是否存在這樣的 “局部” 呢?
• 經過觀察, 發現第1行就是這樣的一個 “局部”
– 因爲第1行的各開關狀態確定的情況下, 這些開關作用過後, 將
導致第1行某些燈是亮的, 某些燈是滅的
要熄滅第1行某個亮着的燈(假設位於第i列), 那麼唯一的辦法就
是按下第2行第i列的開關
(因爲第1行的開關已經用過了, 而第3行及其後的開關不會影響
到第1行)
– 爲了使第1行的燈全部熄滅, 第2行的合理開關狀態就是唯一的
43
解題分析
• 第2行的開關起作用後,
 爲了熄滅第2行的燈, 第3行的合理開關狀態就也是唯一的
 以此類推, 最後一行的開關狀態也是唯一的
• 只要第1行的狀態定下來, 記作A, 那麼剩餘行的情況就是確定唯一
的了
44
解題分析
 推算出最後一行的開關狀態, 然後看看最後一行的開關起作用後,
最後一行的所有燈是否都熄滅:
• 如果是, 那麼A就是一個解的狀態
• 如果不是, 那麼A不是解的狀態, 第1行換個狀態重新試試
• 只需枚舉第1行的狀態, 狀態數是2
6 = 64
45
有沒有狀態數更少的做法?
• 枚舉第一列, 狀態數是2
5 = 32
46

#include <memory>
#include <string>
#include <cstring>
#include <iostream>
using namespace std;
int GetBit(char c,int i) { 
//取c的第i位
return ( c >> i ) & 1; 
}
void SetBit(char & c,int i, int v) {
//設置c的第i位爲v
if( v ) 
c |= ( 1 << i);
else
c &= ~( 1 << i); 
}
void Flip(char & c, int i) { 
//將c的第i位爲取反
c ^= ( 1 << i); 
}
void OutputResult(int t,char result[]) //輸出結果
{
cout << "PUZZLE #" << t << endl;
for( int i = 0;i < 5; ++i ) {
for( int j = 0; j < 6; ++j ) {
cout << GetBit(result[i],j);
if( j < 5 ) 
cout << " ";
}
cout << endl;
}
}
int main() {
char oriLights[5]; //最初燈矩陣,一個比特表示一盞燈
char lights[5]; //不停變化的燈矩陣
char result[5]; //結果開關矩陣
char switchs; //某一行的開關狀態
int T;
cin >> T;
for( int t = 1; t <= T; ++ t) {
memset(oriLights,0,sizeof(oriLights));
for( int i = 0;i < 5; i ++ ) { //讀入最初燈狀態
for( int j = 0; j < 6; j ++ ) {
int s;
cin >> s;
SetBit(oriLights[i],j,s);
}
}
for( int n = 0; n < 64; ++n ) { //遍歷首行開關的64種狀態
memcpy(lights,oriLights,sizeof(oriLights));
switchs = n; //第i行的開關狀態
for( int i = 0;i < 5; ++i ) {
result[i] = switchs; //第i行的開關方案
for( int j = 0; j < 6; ++j ) {
if( GetBit(switchs,j)) {
if( j > 0)
Flip(lights[i],j-1);//改左燈
Flip(lights[i],j);//改開關位置的燈
if( j < 5 )
Flip(lights[i],j+1);//改右燈
}
}
if( i < 4 )
lights[i+1] ^= switchs;//改下一行的燈
switchs = lights[i]; //第i+1行開關方案和第i行燈情況同
} 
if( lights[4] == 0 ) {
OutputResult(t,result);
break;
}
} // for( int n = 0; n < 64; n ++ ) 
}
return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章