NWPU-演算法設計理論作業

Phoenix_ZengHao 發表於 2020-10-28

1.二分查詢

描述

給定一個單調遞增的整數序列,問某個整數是否在序列中。

輸入

第一行為一個整數n,表示序列中整數的個數;第二行為n(n不超過10000)個整數;第三行為一個整數m(m不超過50000),表示查詢的個數;接下來m行每行一個整數k。

輸出

每個查詢的輸出佔一行,如果k在序列中,輸出Yes,否則輸出No。

輸入樣例

5
1 3 4 7 11
3
3
6
9

輸出樣例

Yes
No
No

思路:

方法一:

按照題目要求進行二分查詢,由於a[]是單增數列,所以對於每一個輸入查詢x,我們先設定左邊界l=1,有邊界r=n,ans儲存可能的位置,進行二分查詢,每一次二分查詢時mid=(l+r)/2。如果a[mid]>=x,說明如果存在x,那麼x一定在左邊界(包括mid),那麼此時r=mid-1,ans=mid;如果a[mid]<x,則說明x一定在有邊界。二分查詢完之後,要驗證a[ans]是否等於x,因為二分只是由於它是有序序列進行二分,不斷查詢與x最接近的數字,但它不一定能夠是x。

這個方法時間複雜度為O(nlogn).

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=50000+50;
int n,a[maxn],m;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	cin>>m;
	while(m--){
		int x;
		cin>>x;
		int l=1,r=n,ans=0;
		while(l<=r){
			int mid=(l+r)/2;
			if(a[mid]>=x){
				r=mid-1;
				ans=mid;
			}
			else{
				l=mid+1;
			}
		}
		if(a[ans]==x){
			cout<<"Yes"<<endl;
		}
		else{
			cout<<"No"<<endl;
		}
	}
	return 0;
}

方法二:

題目要求很明確,就是問x是否在a[]中存在,那就把a[]中出現過的數字進行標記就可以了,這裡對 a[]中的每一個數字的範圍解釋不夠清楚,所以我們可以用map進行對映處理。當然其實也可以vis[]陣列進行標記,vis[]大小設定為5e5,這樣讓vis[a[i]]=1,然後查詢vis[x]是否等於1就可以了,或者用map標記方法也一樣。這種方法的時間複雜度O(n)(直接陣列標記)或者O(nlogn)(map)。

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<map> 
using namespace std;
const int maxn=50000+50;
map<int,int>mp;
int n,a[maxn],m;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],mp[a[i]]=1;
	cin>>m;
	while(m--){
		int x;
		cin>>x;
		if(mp[x]==1){
			cout<<"Yes"<<endl;
		}
		else{
			cout<<"No"<<endl;d
		}
	}
}

2.求解投骰子游戲問題

問題描述:玩家根據骰子的點數決定走的步數,即骰子點數為1時可以走一步,點數為2時可以走兩步,點數為n時可以走n步。求玩家走到第n步(n≤骰子最大點數且投骰子方法唯一)時總共有多少種投骰子的方法。

輸入描述:輸入包括一個整數n(1≤n≤6)。

輸出描述:輸出一個整數,表示投骰子的方法數。

輸入樣例:6

輸出樣例:32

思路:

方法一:

看到這個題的第一個思路是用遞推/動態規劃。很容易看出來對於狀態i(當前走到第i步)可以由狀態0,1,…,i-1得到(即走i步,走i-1步,…,走1步),設dp[0]=1(即處於原始位置的方式只有一種),那麼dp[i]=dp[i-1]+dp[i-2]+…+dp[0].所以n^ 2列舉即可,當然為了時候的優化,我們還可以進行字首和優化(即每一次得到dp[i-1]都在pre之中,對於dp[i+1]我們就可以O(1)求得),當然這裡n<=6,就不必這麼複雜了。所以寫了一個時間複雜度為O(n^2)的程式碼。

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=20;
int n,dp[maxn];
int main(){
	cin>>n;
	dp[0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=i-1;j++){
			dp[i]+=dp[j];
		}
	}
	cout<<dp[n]<<endl;
	return 0;
}

執行結果:

方法二:

老師出這個題應該是為了考察dfs,所以這樣寫一下dfs的方法:

對於x,與方法一的解釋一樣,它肯定是由0,1,…,x-1得到的,所以我們再dfs求走到第0,1,…,x-1步的方法,dfs的邊界設定為 if(x==0)return 1;這樣也是可以求得結果的,只是時間複雜度是指數級別。但是如果我們進行記憶化搜尋,即用dp[]進行記錄每一個狀態的答案,一旦存在就不再dfs,這樣的時間複雜度可以優化到O(n^2).

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int dfs(int x){
	if(x==0)return 1;
	int res=0;
	for(int i=0;i<=x-1;i++)res+=dfs(i);
	return res;
}
int main(){
	cin>>n;
	cout<<dfs(n)<<endl;
	return 0;
}

3.0-1揹包

描述

需對容量為c 的揹包進行裝載。從n 個物品中選取裝入揹包的物品,每件物品i 的重量為wi ,價值為pi 。對於可行的揹包裝載,揹包中物品的總重量不能超過揹包的容量,最佳裝載是指所裝入的物品價值最高。

輸入

多個測例,每個測例的輸入佔三行。第一行兩個整數:n(n<=10)和c,第二行n個整數分別是w1到wn,第三行n個整數分別是p1到pn。n 和 c 都等於零標誌輸入結束。

輸出

每個測例的輸出佔一行,輸出一個整數,即最佳裝載的總價值。

輸入樣例

1 2
1
1
2 3
2 2
3 4
0 0

輸出樣例

1
4

思路:

方法一:

0-1揹包的裸題,那就可以直接寫一個01揹包的動態轉移方程:dp[j]=max(dp[j],dp[j-w[i]]+p[i])。dp[j]的意思是:當揹包已裝j的重量的物品時的最大價值。那麼它可以由揹包已裝j-w[i]時最大的價值進行轉移,即由dp[j-w[i]]+p[i]得到。注意每一次要將dp[]設定為0,因為揹包此時無價值。當狀態方程列舉結束後,我們再從 dp[]陣列中找一遍,求得答案maxx=max{dp[i]}(i from 0 to c),輸出答案maxx。這種動態規劃的方法的時間複雜度為O(n^2).

ps:0-1揹包也可以寫成二維dp[][],只是這樣寫成滾動陣列可以更加節省空間。

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=	2000+50;
int n,c,w[maxn],dp[maxn],p[maxn];
int main(){
	int i,j;
	while(1){
		scanf("%d%d",&n,&c);
		if(n==0&&c==0)break;
		for(i=1;i<=n;i++)cin>>w[i];
		for(i=1;i<=n;i++)cin>>p[i];
		memset(dp,0,sizeof(dp));
		for(i=1;i<=n;i++){
			for(j=c;j>=1;j--){
				if(j-w[i]>=0&&dp[j]<dp[j-w[i]]+p[i]){
					dp[j]=dp[j-w[i]]+p[i];
				}
			}
		}
		int maxx=0;
		for(i=0;i<=c;i++)
			if(maxx<dp[i])
				maxx=dp[i];
		cout<<maxx<<endl;
	}
	
	return 0;
}

方法二:

除了直接寫0-1揹包的動態轉移方程,還可以直接寫dfs,每一個揹包無非就是取和不取兩個狀態,如果要取則要求揹包容量 res>=w[now]。分別用ans1,ans2表示取當前物品,不取當前物品的最大價值,dfs返回max(ans1,ans2),dfs的終止條件是now ==n+1。時間複雜度(2^n)。

ps:方法二相較於方法一思維上更加簡單,容易想到,但是程式碼就相對麻煩,並且時間複雜度不夠優秀,當然如果加上記憶化搜尋後時間複雜度和動態規劃是相當的。我個人更喜歡方法一。

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=	2000+50;
int n,c,w[maxn],p[maxn];
int dfs(int now,int res){
	if(now==n+1)return 0;
	int ans1=0,ans2=0;
	if(res>=w[now]){
		ans1=dfs(now+1,res-w[now])+p[now];
	}
	ans2=dfs(now+1,res);
	if(ans1>=ans2)return ans1;
	return ans2;
}
int main(){
	int i,j;
	while(1){
		scanf("%d%d",&n,&c);
		if(n==0&&c==0)break;
		for(i=1;i<=n;i++)cin>>w[i];
		for(i=1;i<=n;i++)cin>>p[i];
		cout<<dfs(1,c)<<endl;
	}
	return 0;
}

4.求解組合問題

編寫一個實驗程式,採用回溯法輸出自然數1~n中任取r個數的所有組合。

思路:

用回溯法求組合,n個數字選r個,每一個數字有兩種選擇,要麼選,要麼不選。dfs(int now)表示當前的決策數字是now,則dfs的邊界條件是now=n+1。對於數字now,可以選擇,也可以不選擇。選擇的話a[++a[0]]=now,a[0]記錄當前選擇的數的數量,回溯的時候記得a[0]–,再進行不選擇now的dfs搜尋。當now==n+1的時候表明當前已經選擇完了,如果a[0]==r,則當前的選擇組合剛好是滿足條件的,輸出即可。時間複雜度O(2^n)

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=100+50;
int n,r,a[maxn];
void dfs(int now){
	if(now==n+1){
		if(a[0]==r){
			for(int i=1;i<=a[0];i++)cout<<a[i]<<' ';cout<<endl;
		}
		return ;
	}
	a[++a[0]]=now;
	dfs(now+1);
	a[0]--;
	dfs(now+1);
}
int main(){
	cin>>n>>r;
	dfs(1);
	return 0;
}

5.最小重量機

設某一機器由n個部件組成,每一種部件都可以從m個不同的供應商處購得。設 wij 是從供應商j處購得的部件 i 的重量, cij 是相應的價格。試設計一個演算法,給出總價格不超過 c 的最小重量機器設計。

輸入:第一行3個整數n,m,cost,接下來n行輸入wij (每行m個整數),最後n行輸入cij (每行m個整數),這裡n>=1, m<=100.

輸出:第一行包括n個整數,表示每個對應的供應商編號,第2行為對應的重量。

輸入樣例:

337

123

321

232

123

542

212

輸出樣例:

131

4

思路:

這裡用dfs進行搜尋,每找到一個符合條件的情況,就更新bestw和x[]。bestw儲存最小的重量,x[i]表示部件i所對應的供應商,nowx[i]表示dfs過程中部件i選擇的供應商。w[i][j] 表示從供應商j處購得的部件i的重量 ,c[i][j]表示相應的價格。dfs(int now,int noww,int nowp)表示搜尋第now個部件時的當前重量為noww,耗費nowp,對於第now個物品它有m個供應商可以選擇,當nowp+c[now][i]<=cost時則符合條件可以進行dfs(now+1,noww+w[now][i],nowp+c[now][i]);dfs的邊界條件時now==n+1,此時如果bestw>noww,則要更新bestw,以及x[]。最後輸出bestw和x[].

ps:如果PE的話,把最後的空格去掉(輸出x[]的時候,x[n]後面不要跟空格),並且加上printf("\n").

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=200+50;
int n,m,cost,w[maxn][maxn],c[maxn][maxn],bestw=0x3f3f3f3f,nowx[maxn],x[maxn];
void dfs(int now,int noww,int nowp){
	int i,j;
	if(now==n+1){
		if(bestw>noww){
			bestw=noww;
			for(i=1;i<=n;i++)x[i]=nowx[i];
		}
		return ;
	}
	for(i=1;i<=m;i++){
		if(nowp+c[now][i]<=cost){
			nowx[now]=i;
			dfs(now+1,noww+w[now][i],nowp+c[now][i]);
		}
	}
}
int main(){
	cin>>n>>m>>cost;
	int i,j;
	for(i=1;i<=n;i++)
		for(j=1;j<=m;j++)	
			cin>>w[i][j];
	for(i=1;i<=n;i++)
		for(j=1;j<=m;j++)
			cin>>c[i][j];
	dfs(1,0,0);
	for(i=1;i<=n;i++){
		cout<<x[i];
		if(i!=n)cout<<' ';
	}
	cout<<endl;
	cout<<bestw<<endl;
	return 0;
}

6.最長公共子序列

描述

一個給定序列的子序列是在該序列中刪去若干元素後得到的序列。確切地說,若給定序列X=<x1, x2,…, xm>,則另一序列Z=<z1, z2,…, zk>是X的子序列是指存在一個嚴格遞增的下標序列 <i1, i2,…, ik>,使得對於所有j=1,2,…,k有:
Xij = Zj
如果一個序列S即是A的子序列又是B的子序列,則稱S是A、B的公共子序列。
求A、B所有公共子序列中最長的序列的長度。

輸入

輸入共兩行,每行一個由字母和數字組成的字串,代表序列A、B。A、B的長度不超過200個字元。

輸出

一個整數,表示最長各個子序列的長度。

格式:printf("%d\n");

輸入樣例

programming
contest

輸出樣例

2

思路:

像LCS這樣的問題,它具有重疊子問題的性質,因此:用遞迴來求解就太不划算了。因為採用遞迴,它重複地求解了子問題。採用動態規劃時,並不需要去一 一 計算那些重疊了的子問題 。對於**dp[i][j]**表示 (s1,s2…si) 和 (t1,t2…tj) 的最長公共子序列的長度

當i== 0|| j== 0 時,dp[i][j]=0;

當i,j>0,si==tj時, dp[i][j]=dp[i-1][j-1]+1;

當i,j>0,si!=tj時,dp[i][j]=max{dp[i][j-1],dp[i-1][j]};

時間複雜度:

由於只需要填一個m行n列的二維陣列,其中m代表第一個字串長度,n代表第二個字串長度

所以時間複雜度為O(m*n)

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=1000+50;
char s[maxn],t[maxn];int lens,lent,dp[maxn][maxn];
int max(int x,int y){
	if(x>y)return x;
	return y;
}
int main(){
	scanf("%s%s",s+1,t+1);
	lens=strlen(s+1);
	lent=strlen(t+1);
	for(int i=1;i<=lens;i++){
		for(int j=1;j<=lent;j++){
			if(s[i]==t[j]){
				dp[i][j]=dp[i-1][j-1]+1;
			}
			else{
				dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
			}
		}
	}
	printf("%d\n",dp[lens][lent]);
	return 0;
}

ps:這個NOJ就離譜,我寫成maxn=1e3+50,執行時錯誤,刷屏了一頁,最後才發現編譯器無法識別1e3,這編譯器是有多垃圾???

7.活動安排問題

問題:有n個活動的集合A={1,2,…,n},其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。
求解:安排儘量多項活動在該場地進行,即求A的最大相容子集。
設待安排的11個活動的開始時間和結束時間按結束時間的升序排列如下:

將此表資料作為實現該演算法的測試資料。

i 1 2 3 4 5 6 7 8 9 10 11
s[i] 1 3 0 5 3 5 6 8 8 2 12
f[i] 4 5 6 7 8 9 10 11 12 13 14

思路:

方法一:

題意很明顯希望參加的活動數目儘量多。對於活動安排問題可以採取動態規劃的策略:

首先將活動的結束時間按照第一關鍵字排序(由小到大),再將活動的開始時間作為第二關鍵字排序(由小到大)

定義dp[i]表示在前i場比賽中最多可以參加幾場比賽,

由此得出方程:dp[i]=max(dp[i-1],dp[temp]+1);

temp指從dp[i-1]向前找到的第一個允許參加第i場活動的活動編號,由它推匯出dp[i]=dp[temp]+1;

由於每次迴圈時都向前找一次temp會浪費太多時間,又因為活動開始或結束時間是單調遞增的,

故可以令temp在迴圈時逐步遞增,這樣時間複雜度就降到了O(n).

這裡只是講解一下動態規劃的想法。就不寫程式碼了,動態規劃的程式碼和下面的貪心方法相似。只是這種動態規劃的思路是基於貪心的思想來實現的。

方法二:

這個問題可以抽象為在一個數軸上有n條線段,現要選取其中k條線段使得這k條線段兩兩沒有重合部分,問最大的k為多少。
最左邊的線段放什麼最好?
顯然放右端點最靠左的線段最好,從左向右放,右端點越小妨礙越少。
其他線段放置按右端點排序,貪心放置線段,即能放就放。

以上兩種方法的時間複雜度都是O(nlogn),快速排序的時間複雜度是O(nlogn),而動態規劃或者貪心執行更新策略的時間複雜度是O(n).

程式碼:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=2e3+50;
int n;
struct Node{
	int s,f;
}a[maxn];
bool cmp(Node x,Node y){
	if(x.f==y.f)return x.s<y.s;
	return x.f<y.f;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i].s>>a[i].f;
	sort(a+1,a+1+n,cmp);
	int ans=1,now=1,opt=2;
	while(opt<=n){
		if(a[opt].s>=a[now].f){
			ans++;
			now=opt;
		}
		opt++;
	}
	cout<<ans<<endl;
	return 0;
}