遞推演算法與二分演算法
遞推演算法與二分演算法
遞推演算法:
(一)斐波那契數列
以下數列0 1 1 2 3 5 8 13 21 …被稱為斐波納契數列。
這個數列從第3項開始,每一項都等於前兩項之和。
輸入一個整數N,請你輸出這個序列的前N項。
輸入格式
一個整數N。
輸出格式
在一行中輸出斐波那契數列的前N項,數字之間用空格隔開。
資料範圍
0<N<460<N<46
輸入樣例:
5
輸出樣例:
0 1 1 2 3
#include<iostream> using namespace std; int f[100]; int main() { int i,j,n; cin>>n; f[1]=0,f[2]=1,f[3]=1; for(i=3;i<=n;i++) f[i]=f[i-1]+f[i-2]; for(i=1;i<=n;i++) cout<<f[i]<<" "; return 0; }
(二)費解的開關
你玩過“拉燈”遊戲嗎?25盞燈排成一個5x5的方形。每一個燈都有一個開關,遊戲者可以改變它的狀態。每一步,遊戲者可以改變某一個燈的狀態。遊戲者改變一個燈的狀態會產生連鎖反應:和這個燈上下左右相鄰的燈也要相應地改變其狀態。
我們用數字“1”表示一盞開著的燈,用數字“0”表示關著的燈。下面這種狀態
10111
01101
10111
10000
11011
在改變了最左上角的燈的狀態後將變成:
01111
11101
10111
10000
11011
再改變它正中間的燈後狀態將變成:
01111
11001
11001
10100
11011
給定一些遊戲的初始狀態,編寫程式判斷遊戲者是否可能在6步以內使所有的燈都變亮。
輸入格式
第一行輸入正整數n,代表資料中共有n個待解決的遊戲初始狀態。
以下若干行資料分為n組,每組資料有5行,每行5個字元。每組資料描述了一個遊戲的初始狀態。各組資料間用一個空行分隔。
輸出格式
一共輸出n行資料,每行有一個小於等於6的整數,它表示對於輸入資料中對應的遊戲狀態最少需要幾步才能使所有燈變亮。
對於某一個遊戲初始狀態,若6步以內無法使所有燈變亮,則輸出“-1”。
資料範圍
0<n≤500
輸入樣例:
3 00111 01011 10001 11010 11100 11101 11101 11110 11111 11111 01111 11111 11111 11111 11111
輸出樣例:
3
2
-1
思路:我們可以來列舉第一行的操作,第一行一共有5個,所以有52個操作,然後第二行根據他這一列,第一行是“0”的進行按下開關操作,
以此類推,到最後一行時,直接判斷是否全為“1”,如果全為“1”,則代表可以完成,在判斷操作次數是否大於6,否則不可以完成。
程式碼:
#include<iostream> #include<cstring> using namespace std; char g[7][7]; int dx[5]={-1,0,1,0,0},dy[5]={0,1,0,-1,0}; //判斷函式 void turn(int x,int y) { for(int i=0;i<5;i++) { int a=x+dx[i]; int b=y+dy[i]; if(a<0||a>=5||b<0||b>=5) continue; g[a][b]^=1;//異或操作,將原來是1的變為0,原來是0的變為1 } } int main() { int t,i,j; cin>>t; while(t--) { for(i=0;i<5;i++) cin>>g[i];//存入字串 char black[7][7]; int ans=100000; for(int op=0;op<32;op++)//模擬第一行一共有2的5次方操作選擇,每一位都有兩種選擇,故32中選擇 { int step=0; memcpy(black,g,sizeof(g));//儲存一份原先的字串陣列 for(j=0;j<5;j++) { if(op >> j & 1)//判斷該位是否有1,有1則進行操作 { step++; turn(0,j); } } //要對前4排進行判斷,如果為0則後一排就操作,否則不操作 for(i=0;i<4;i++) { for(j=0;j<5;j++) { if(g[i][j]=='0') { step++; turn(i+1,j); } } } //最後看最後一排是否全為1 bool flag=false; for(i=0;i<5;i++) { if(g[4][i]=='0') { flag=true; break; } } //更新最小步數 if(!flag) ans=min(ans,step); //還原字串陣列 memcpy(g,black,sizeof(black)); } //如果超出6步,則更改為-1 if(ans>6) ans=-1; cout<<ans<<endl; } return 0; }
(三)翻硬幣
小明正在玩一個“翻硬幣”的遊戲。
桌上放著排成一排的若干硬幣。我們用 * 表示正面,用 o 表示反面(是小寫字母,不是零)。
比如,可能情形是:**oo***oooo
如果同時翻轉左邊的兩個硬幣,則變為:oooo***oooo
現在小明的問題是:如果已知了初始狀態和要達到的目標狀態,每次只能同時翻轉相鄰的兩個硬幣,那麼對特定的局面,最少要翻動多少次呢?
我們約定:把翻動相鄰的兩個硬幣叫做一步操作。
輸入格式
兩行等長的字串,分別表示初始狀態和要達到的目標狀態。
輸出格式
一個整數,表示最小操作步數
資料範圍
輸入字串的長度均不超過100。
資料保證答案一定有解。
輸入樣例1:
**********
o****o****
輸出樣例1:
5
輸入樣例2:
*o**o***o***
*o***o**o***
輸出樣例2:
1
思路:其實我們可以發現最少翻動的次數是固定的,我們的操作時從前往後進行字串的對比,
如果有不一樣的,就進行翻轉,且前一個的翻轉只能影響後一個的狀態,這樣的步數就是最小的同時也是固定的
程式碼:
#include<iostream> using namespace std; int main() { string a,b; int i,j; cin>>a>>b; int ans=0; for(i=0;i<a.length();i++) { if(a[i]==b[i]) continue; else { ans++; for(j=i;j<=i+1;j++) { if(a[j]=='*') a[j]='o'; else a[j]='*'; } } } cout<<ans<<endl; return 0; }
(四)飛行員兄弟
“飛行員兄弟”這個遊戲,需要玩家順利的開啟一個擁有16個把手的冰箱。
已知每個把手可以處於以下兩種狀態之一:開啟或關閉。
只有當所有把手都開啟時,冰箱才會開啟。
把手可以表示為一個4х4的矩陣,您可以改變任何一個位置[i,j]上把手的狀態。
但是,這也會使得第i行和第j列上的所有把手的狀態也隨著改變。
請你求出開啟冰箱所需的切換把手的次數最小值是多少。
輸入格式
輸入一共包含四行,每行包含四個把手的初始狀態。
符號“+”表示把手處於閉合狀態,而符號“-”表示把手處於開啟狀態。
至少一個手柄的初始狀態是關閉的。
輸出格式
第一行輸出一個整數N,表示所需的最小切換把手次數。
接下來N行描述切換順序,每行輸入兩個整數,代表被切換狀態的把手的行號和列號,數字之間用空格隔開。
注意:如果存在多種開啟冰箱的方式,則按照優先順序整體從上到下,同行從左到右開啟。
資料範圍
1≤i,j≤4
輸入樣例:
-+--
----
----
-+--
輸出樣例:
6
1 1
1 3
1 4
4 1
4 3
4 4
思路:對於這個4*4的矩陣,我們的操作有216次,我們可以進行暴力列舉
// 0 1 2 3
// 4 5 6 7
// 8 9 10 11
// 12 13 14 15
//對每一位操作來判斷
用運算元來對陣列中的每一位進行判斷是否要進行該操作,
操作後記錄操作的路徑,進行操作時注意該操作位進行了兩次轉換,狀態不變因此我們需要再進行一次這個位置的單獨轉換
最後判斷是否有無“+”,然後更新最優陣列。
程式碼:
#include<iostream> #include<vector> #include<cstring> using namespace std; #define x first //重定義first和second為x和y #define y second char g[6][6],black[6][6]; //得到陣列的對應值 // 0 1 2 3 // 4 5 6 7 // 8 9 10 11 // 12 13 14 15 int get(int x,int y) { return 4*x+y; } //轉化每一位 void turn_one(int x,int y) { if(g[x][y]=='-') g[x][y]='+'; else g[x][y]='-'; } //轉換所有 void turn_all(int x,int y) { for(int i=0; i<4; i++) { turn_one(x,i); turn_one(i,y); } turn_one(x,y); } int main() { for(int i=0; i<4; i++) { cin>>g[i]; } vector< pair<int,int> > ans; //一共有2的16次方個運算元,對應每一位都有2個選擇,一共有16個數 for(int op=0; op < 1<<16; op++) { vector< pair<int,int> > temp;//用來儲存資料操作 memcpy(black,g,sizeof(g));//設定一個用來還原的陣列 for(int i=0; i<4 ; i++) { for(int j=0; j<4; j++) { // 0 1 2 3 // 4 5 6 7 // 8 9 10 11 // 12 13 14 15 //對每一位操作來判斷 if(op>>get(i,j)&1) { temp.push_back({i,j});//放到數組裡 turn_all(i,j);//轉換該點的每一行和每一列 } } } //判斷是否有無+號 bool flag=false; for(int i=0; i<4; i++) { for(int j=0; j<4; j++) { if(g[i][j]=='+') { flag=true; break; } } } //更新最優陣列 if(!flag) { if(ans.empty()||ans.size()>temp.size()) ans=temp; } //再將陣列還原 memcpy(g,black,sizeof(g)); } cout<<ans.size()<<endl; for(int i=0; i<ans.size(); i++) { cout<<ans[i].x+1<<" "<<ans[i].y+1<<endl; } return 0; } #include<iostream> #include<vector> #include<cstring> using namespace std; #define x first //重定義first和second為x和y #define y second char g[6][6],black[6][6]; //得到陣列的對應值 // 0 1 2 3 // 4 5 6 7 // 8 9 10 11 // 12 13 14 15 int get(int x,int y) { return 4*x+y; } //轉化每一位 void turn_one(int x,int y) { if(g[x][y]=='-') g[x][y]='+'; else g[x][y]='-'; } //轉換所有 void turn_all(int x,int y) { for(int i=0; i<4; i++) { turn_one(x,i); turn_one(i,y); } turn_one(x,y); } int main() { for(int i=0; i<4; i++) { cin>>g[i]; } vector< pair<int,int> > ans; //一共有2的16次方個運算元,對應每一位都有2個選擇,一共有16個數 for(int op=0; op < 1<<16; op++) { vector< pair<int,int> > temp;//用來儲存資料操作 memcpy(black,g,sizeof(g));//設定一個用來還原的陣列 for(int i=0; i<4 ; i++) { for(int j=0; j<4; j++) { // 0 1 2 3 // 4 5 6 7 // 8 9 10 11 // 12 13 14 15 //對每一位操作來判斷 if(op>>get(i,j)&1) { temp.push_back({i,j});//放到數組裡 turn_all(i,j);//轉換該點的每一行和每一列 } } } //判斷是否有無+號 bool flag=false; for(int i=0; i<4; i++) { for(int j=0; j<4; j++) { if(g[i][j]=='+') { flag=true; break; } } } //更新最優陣列 if(!flag) { if(ans.empty()||ans.size()>temp.size()) ans=temp; } //再將陣列還原 memcpy(g,black,sizeof(g)); } cout<<ans.size()<<endl; for(int i=0; i<ans.size(); i++) { cout<<ans[i].x+1<<" "<<ans[i].y+1<<endl; } return 0; }
二分演算法
(一)數的範圍
給定一個按照升序排列的長度為n的整數陣列,以及 q 個查詢。
對於每個查詢,返回一個元素k的起始位置和終止位置(位置從0開始計數)。
如果陣列中不存在該元素,則返回“-1 -1”。
輸入格式
第一行包含整數n和q,表示陣列長度和詢問個數。
第二行包含n個整數(均在1~10000範圍內),表示完整陣列。
接下來q行,每行包含一個整數k,表示一個詢問元素。
輸出格式
共q行,每行包含兩個整數,表示所求元素的起始位置和終止位置。
如果陣列中不存在該元素,則返回“-1 -1”。
資料範圍
1≤n≤100000
1≤q≤10000
1≤k≤10000
輸入樣例:
6 3
1 2 2 3 3 4
3
4
5
輸出樣例:
3 4
5 5
-1 -1
思路:找起始位置實際上就是求:f[i]>=x;而查詢最終位置實際上是在求:f[i]<=x
程式碼:
#include<cstdio> #include<iostream> using namespace std; const int N=100010; int f[N]; int n,q; int main() { int i,j; cin>>n>>q; for(i=0;i<n;i++) { scanf("%d",&f[i]); } while(q--) { int x; scanf("%d",&x); int l=0,r=n-1; while(l<r) { int mid=(l+r)/2; if(f[mid]>=x) r=mid; else l=mid+1; } if(f[r]==x) { cout<<r<<" "; r=n-1; while(l<r) { int mid=(l+r+1)/2; if(f[mid]<=x) l=mid; else r=mid-1; } cout<<l<<endl; } else { cout<<"-1 -1"<<endl; } } return 0; }
(二)數的三次方根
給定一個浮點數n,求它的三次方根。
輸入格式
共一行,包含一個浮點數n。
輸出格式
共一行,包含一個浮點數,表示問題的解。
注意,結果保留6位小數。
資料範圍
−10000≤n≤10000
輸入樣例:
1000.00
輸出樣例:
10.000000
程式碼:
#include<iostream> #include<cstdio> using namespace std; int main() { double x; cin>>x; double l=-10000,r=10000; while(r-l>1e-8) { double mid=(l+r)/2; if(mid*mid*mid>=x) r=mid; else l=mid; } printf("%lf",r); return 0; }
(三)機器人跳躍問題
機器人正在玩一個古老的基於DOS的遊戲。
遊戲中有N+1座建築——從0到N編號,從左到右排列。
編號為0的建築高度為0個單位,編號為 i 的建築高度為H(i)個單位。
起初,機器人在編號為0的建築處。
每一步,它跳到下一個(右邊)建築。
假設機器人在第k個建築,且它現在的能量值是E,下一步它將跳到第k+1個建築。
如果H(k+1)>E,那麼機器人就失去H(k+1)-E的能量值,否則它將得到E-H(k+1)的能量值。
遊戲目標是到達第N個建築,在這個過程中能量值不能為負數個單位。
現在的問題是機器人以多少能量值開始遊戲,才可以保證成功完成遊戲?
輸入格式
第一行輸入整數N。
第二行是N個空格分隔的整數,H(1),H(2),…,H(N)代表建築物的高度。
輸出格式
輸出一個整數,表示所需的最少單位的初始能量值。
資料範圍
1≤N,H(i)≤105
輸入樣例1:
5
3 4 3 2 4
輸出樣例1:
4
輸入樣例2:
3
4 4 4
輸出樣例2:
4
輸入樣例3:
3
1 6 4
輸出樣例3:
3
思路:這裡要注意的是x可能會爆,因此當x大於最大值時,必然是成立的,我們就可以返回true
程式碼:
#include<iostream> using namespace std; typedef long long ll; ll n,h[100010],i,j,maxn=-1; bool check(ll x) { bool flag=true; for(ll i=0;i<n;i++) { if(h[i]>x) x=x-(h[i]-x); else x=x+(x-h[i]); if(x<0) { flag=false; break; } if(x>maxn) break; } return flag; } int main() { cin>>n; for(i=0;i<n;i++) { cin>>h[i]; maxn=max(maxn,h[i]); } ll l=0,r=100000; while(l<r) { ll mid=l+(r-l)/2; if(check(mid)) r=mid; else l=mid+1; } cout<<r<<endl; return 0; }
(四)分巧克力
兒童節那天有 K 位小朋友到小明家做客。
小明拿出了珍藏的巧克力招待小朋友們。
小明一共有 N 塊巧克力,其中第 i 塊是 Hi×Wi 的方格組成的長方形。
為了公平起見,小明需要從這 N 塊巧克力中切出 K 塊巧克力分給小朋友們。
切出的巧克力需要滿足:
- 形狀是正方形,邊長是整數
- 大小相同
例如一塊 6×5的巧克力可以切出 6 塊 2×2 的巧克力或者 2 塊 3×3 的巧克力。
當然小朋友們都希望得到的巧克力儘可能大,你能幫小明計算出最大的邊長是多少麼?
輸入格式
第一行包含兩個整數 N 和 K。
以下 N 行每行包含兩個整數 Hi 和 Wi。
輸入保證每位小朋友至少能獲得一塊 1×11×1 的巧克力。
輸出格式
輸出切出的正方形巧克力最大可能的邊長。
資料範圍
1≤N,K≤105
1≤Hi,Wi≤105
輸入樣例:
2 10
6 5
5 6
輸出樣例:
2
思路:注意這裡不可以用面積來進行判斷,而應該分別用長和寬來分別除以邊長,然後相乘
程式碼:
#include<iostream> using namespace std; int n,k,s[100010]; int h[100010],w[100010]; bool check(int x) { int ans=0; for(int i=0;i<n;i++) { ans+=(h[i]/x)*(w[i]/x);//來計算可以分給多少人 } if(ans>=k) return true; else return false; } int main() { int i,j,maxn=-1; cin>>n>>k; for(i=0;i<n;i++) { cin>>h[i]>>w[i]; } int l=0,r=100010; while(l<r) { int mid=(l+r+1)/2;//加1操作,避免死迴圈 if(check(mid)) l=mid; else r=mid-1; } cout<<l<<endl; return 0; }
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
收穫:對二分的兩種方法的掌握理解的更加滲透,理解二分的時候什麼時候需要+1,避免死迴圈;同時掌握了一些遞推的思路和尋求方法
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
藍橋杯第二次訓練
2019.12.10
xlf