一.遞歸與分治
1.大整數的乘法
將n位的整數分成兩段,每段長度數 n/2
package algorithm_2019;
import java.math.BigInteger;
import java.util.Scanner;
public class 分治法求大整數乘法{
static Scanner cin = new Scanner(System.in);
static BigInteger Sign(BigInteger a) {//符號
if(a.compareTo(BigInteger.ZERO) > 0)
return BigInteger.ONE;
else return BigInteger.valueOf(-1);
}
static int Max(int a,int b) {//比較大小
if(a>b) return a;
else return b;
}
static BigInteger multi(BigInteger a,BigInteger b,int len_a,int len_b) {
BigInteger sign = Sign(a).multiply(Sign(b));
a = a.abs();
b = b.abs();
if(a.compareTo(BigInteger.ZERO) == 0 || b.compareTo(BigInteger.ZERO) == 0)
return BigInteger.ZERO;
else if(len_a == 1 || len_b == 1)
return sign.multiply(a.multiply(b));
else {
int half = Max(len_a,len_b) / 2;
int lena = len_a - half;
int lenb = len_b - half;
BigInteger tmp1 = BigInteger.valueOf(10).pow(half);
BigInteger tmp2 = BigInteger.valueOf(10).pow(2*half);
BigInteger A = a.divide(tmp1);
BigInteger B = a.mod(tmp1);
BigInteger C = b.divide(tmp1);
BigInteger D = b.mod(tmp1);
BigInteger AC = multi(A,C,lena,lenb);
BigInteger BD = multi(B,D,half,half);
BigInteger num1 = A.subtract(B);
BigInteger num2 = D.subtract(C);
int len1 = num1.abs().toString().length();
int len2 = num2.abs().toString().length();
BigInteger ABCD = multi(num1,num2,len1,len2).add(AC).add(BD);
return sign.multiply(AC.multiply(tmp2).add(ABCD.multiply(tmp1)).add(BD));
}
}
public static void main(String[] args) {
BigInteger a = cin.nextBigInteger();
BigInteger b = cin.nextBigInteger();
String str_a = a.toString();
String str_b = b.toString();
System.out.println("分治法求大整數乘法:\n"+multi(a,b,str_a.length(),str_b.length()));
}
}
2.Strassen矩陣乘法
3.棋盤覆蓋
4.合併排序
遞歸的實現:
算法設計的思想就是,將一個大的序列劃分成兩個小的左右序列,然後這樣遞歸下去,先完成小的序列的有序化,再將兩個有序化的小的序列合併,完成大的序列的有序化。這裏需要注意的就是在將兩個有序的子數組有序的合併到一起的時候,可能某個子數組會有剩下的元素,沒有被轉移到新數組中,這裏就需要在最後加上一個判斷,來將兩個子數組中可能剩下的元素都轉移到新數組中
void merge_sort(vector<int> &a,int l,int r){
if(l>=r)
return;
int mid=(l+r)/2;
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);//左半部分和右半部分排序
static vector<int> w;
w.clear();
int i=l,j=mid+1;
while(i<=mid&&j<=r){//將兩個有序數組合並
if(a[i]<=a[j])
w.push_back(a[i++]);
else
w.push_back(a[j++]);
}
while(i<=mid) w.push_back(a[i++]);//餘下的部分添加到最後
while(j<=r) w.push_back(a[j++]);
for(int i=l,j=0;j<w.size();i++,j++)//轉移
a[i]=w[j];
}
非遞歸的實現:
歸併排序的非遞歸的實現就是不將數組劃分,而是直接先將數組a 中相鄰元素兩兩配對,用合併算法將他們排序,構成n/2組長度爲2的排好序的子數組段,再將他們排序成長度爲4的排好序的子數組段,如此下去,直至整個數組排好序
#include<iostream>
using namespace std;
template<typename T>
void Merge(T a[], T p[], int l, int m, int r){
int left = l, j = m + 1, k = l;
while ((left <= m) && (j <= r)){
if (a[left] <= a[j])
p[k++] = a[left++];
else
p[k++] = a[j++];
}
if (left > m)
for (int q = j; q <= r; q++)
p[k++] = a[q];
if (j > r)
for (int q = left; q <= m; q++)
p[k++] = a[q];
}
template<typename T>
void MergePass(T a[], T p[], int s, int b){
int i = 0;
while (i <= b - 2 * s){
Merge(a,p,i,i+s-1,i+2*s-1);
i = i + 2 * s;
}
if (i + s < b)
Merge(a,p,i,i+s-1,b-1);
else
for (int q = i; q < b; q++)
p[q] = a[q];
}
template<typename T>
void MergeSort(T a[], int b){
T* p = new T[b];
int s = 1;
while (s < b){
MergePass(a,p,s,b);
s += s;
MergePass(p,a,s,b);
s += s;
}
}
int main(){
int m;
cin >> m;
int* a = new int[m];
for (int i = 0; i < m; i++)
cin >> a[i];
MergeSort(a, m);
for (int i = 0; i < m; i++)
cout << a[i] << ' ';
cout<<endl;
return 0;
}
歸併排序思想解決逆序對問題:
這裏用到了歸併的思想,你需要求出左邊部分的逆序對數,然後求出右邊部分的逆序對數,然後還要求出左半部分和右半部分彙總起來的時候的逆序對數,因爲是和歸併排序一樣的實現,所以這個算法的時間複雜度是O(nlogn)。
需要注意的就是,在分別求出左邊和右邊的逆序對數的時候,還得再求出左右兩部分的逆序對數量,因爲這裏是對數組進行從大到小的排序,當右半部分比左半部分的某個元素小的時候,它一定比左邊元素後邊的所有元素都小。
int find(vector<int> &a,int l,int r){
if(l>=r)
return 0;
int res=0;
int mid=(l+r)/2;
res+=find(a,l,mid);//左
res+=find(a,mid+1,r);//右
static vector<int> w;
w.clear();
int i=l,j=mid+1;
while(i<=mid&&j<=r){//交叉排序
if(a[i]<=a[j])
w.push_back(a[i++]);
else{
res+=mid-i+1;//!!! a[j] 比 a[i] 小的時候,一定也比後邊的都小
w.push_back(a[j++]);
}
}
while(i<=mid) w.push_back(a[i++]);//剩餘部分
while(j<=r) w.push_back(a[j++]);
for(int i=l,j=0;j<w.size();i++,j++)
a[i]=w[j];
return res;
}
5.快速排序
def swap(a,b):
t=a
a=b
b=t
def QuickSort(a, l, r):
if l>r:
return -1
temp=a[l]
i=l
j=r
while i!=j:
while a[j]>=temp and i<j:
j-=1
while a[i]<=temp and i<j:
i+=1
if i<j:
swap(a[i],a[j])
a[l]=a[i]
a[i]=temp
QuickSort(a,l,i-1)
QuickSort(a,i+1,r)
a=input().strip().split()
a=[int(i) for i in a]
QuickSort(a,0,len(a)-1)
print(a)
6.線性時間選擇
7.最接近點對問題
8.循環賽日程表
void GameTable(int k){
int n=2;
a[1][1]=1;
a[1][2]=2;
a[2][1]=2;
a[2][2]=1;//預先把左上角設置好
for(int t=2;t<k;t++){
int temp=n;//2^(k-1)
n=n*2;
for(int i=temp+1;i<=n;i++){//左下角
for(int j=1;j<=temp;j++){
a[i][j]=a[i-temp][j]+temp;
}
}
for(int i=1;i<=temp;i++){//右上角
for(int j=temp+1;j<=n;j++){
a[i][j]=a[i+temp][j-temp];
}
}
for(int i=temp+1;i<=n;i++){//右下角
for(int j=temp+1;j<=n;j++){
a[i][j]=a[i-temp][j-temp];
}
}
}
}
void Print(int k){//打印函數
int sum=1;
for(int i=1;i<k;i++)
sum*=2;
for(int i=1;i<=sum;i++){
for(int j=1;j<=sum;j++){
cout<<a[i][j]<<" ";
}
cout<<endl;
}
}
分治法實現:
按分治策略,我們可以將所有的選手分爲兩半,則n個選手的比賽日程表可以通過n/2個選手的比賽日程表來決定。遞歸地用這種一分爲二的策略對選手進行劃分,直到只剩下兩個選手時,比賽日程表的制定就變得很簡單。這時只要讓這兩個選手進行比賽就可以了。如上圖,所列出的正方形表是8個選手的比賽日程表。其中左上角與左下角的兩小塊分別爲選手1至選手4和選手5至選手8前3天的比賽日程。據此,將左上角小塊中的所有數字按其相對位置抄到右下角,又將左下角小塊中的所有數字按其相對位置抄到右上角,這樣我們就分別安排好了選手1至選手4和選手5至選手8在後4天的比賽日程。依此思想容易將這個比賽日程表推廣到具有任意多個選手的情形
#include<iostream>
using namespace std;
int pre[1005][1005];
void print(int n){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
cout<<pre[i][j]<<" ";
}
cout<<endl;
}
}
void copy(int x1,int y1,int x2,int y2,int n){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
pre[x2+i][y2+j]=pre[x1+i][y1+j];
}
}
}
void range_shedule(int x,int y,int n){
if(n==1)
return;
range_shedule(x,y,n/2);
range_shedule(x,y+(n/2),n/2);
copy(x,y,x+n/2,y+n/2,n/2);
copy(x,y+n/2,x+n/2,y,n/2);
}
int main(){
int k;
cout<<"輸入k"<<endl;
cin>>k;
int n=1;
for(int i=1;i<k;i++){
n*=2;
}
for(int i=0;i<n;i++){
pre[i][0]=i+1;
pre[0][i]=i+1;
}
range_shedule(0,0,n);
print(n);
return 0;
}
二.動態規劃
1.矩陣連乘問題
若使用窮舉搜索法,其複雜度是隨着n的增長呈指數增長的,可以使用動態規劃的算法來解決,即求其最優子結構。
設Ai Ai+1…Aj記作A[i:j],設A[i:j] 1<=i <= j <= n,所需要的最少數乘次數爲dp[i][j].則有:
dp[i][j] = 0, i = j;
dp[i][j] = min{dp[i][k] + dp[k+1][j] + pi-1*pk*pj }, i < j .(枚舉k的位置)
path[][]用來記錄最優解的位置,最後通過遞歸的方式,確定最後的計算次序。
#include<iostream>
#include<cstring>
using namespace std;
int p[1005];
int dp[1005][1005];
int path[1005][1005];
int n=5;
void Traceback(int i,int j){//輸出路徑
if(i==j)
return ;
Traceback(i,path[i][j]);
Traceback(path[i][j]+1,j);
cout<<"Multipaly A ("<<i<<","<<path[i][j];
cout<<") and A ("<<path[i][j]+1<<","<<j<<")"<<endl;
}
int main(){
int t;
cin>>t;
while(t--){
for(int i=0;i<=n;i++){
cin>>p[i];
}
memset(dp,0,sizeof(dp));
memset(path,0,sizeof(path)); //初始化
for(int i=1;i<=n;i++){
dp[i][i]=0;
}
for(int r=2;r<=n;r++){//第幾個對角線
for(int i=1;i<=n-r+1;i++){//對角線上的元素個數(上三角)
int j=i+r-1;
dp[i][j]=dp[i+1][j]+p[i-1]*p[i]*p[j];
path[i][j]=i;
for(int k=i+1;k<j;k++){//取k
int t=dp[i][k]+dp[k+1][j]+p[i-1]*p[k]*p[j];
if(t<dp[i][j]){
dp[i][j]=t;
path[i][j]=k;
}
}
}
}
cout<<dp[1][n]<<endl;
Traceback(1,n);
cout<<endl;
}
}
2.最長公共子序列
設序列X={X1,X2 ...... Xm}和Y={Y1,Y2.......Yn}的最長公共子序列爲Z={Z1,Z2......Zk}則
從上述的結論可以看出,兩個序列的最長公共子序列問題包含兩個序列的前綴的最長公共子序列,因此,最長公共子序列問題具有最優子結構性質,在設計遞歸算法時,不難看出遞歸算法具有子問題重疊的性質
設C[i,j]表示Xi 和Yj 的最長公共子序列的長度,如果i=0或j=0即一個序列長度爲0時,那麼最長公共子序列的長度就是0,根據最長公共子序列問題的最優子結構的性質,可以有以下公式:
使用動態規劃算法解決此問題,可採用從簡單到複雜的方式進行計算。在計算的過程中,保存已解決的子問題答案。每個子問題只計算一次,而在後邊需要時只需要簡單的查一下,從而避免大量的重複計算。還是用了path數組存下了路徑信息,在計算結束後可以採用遞歸的算法打印出最長公共子序列。
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
char x[1005],y[1005];
int dp[1005][1005];
int path[1005][1005];
int t;
void print_path(int i,int j){//打印路徑
if(i==0||j==0)
return ;
if(path[i][j]==1){
print_path(i-1,j-1);
cout<<x[i];
}else if(path[i][j]==2)
print_path(i-1,j);
else
print_path(i,j-1);
}
int main(){
cin>>t;
while(t--){
scanf("%s%s",x,y);
memset(dp,0,sizeof(dp));
memset(path,0,sizeof(path));//初始化
int n=strlen(x);
int m=strlen(y);
for(int i=n;i>=0;i--)//將兩個字符序列改成由下標1開始
x[i]=x[i-1];
for(int j=m;j>=0;j--)
y[j]=y[j-1];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(x[i]==y[j]){
dp[i][j]=dp[i-1][j-1]+1;
path[i][j]=1;
}
else if(dp[i-1][j]>=dp[i][j-1]){
dp[i][j]=dp[i-1][j];
path[i][j]=2;
}else{
dp[i][j]=dp[i][j-1];
path[i][j]=3;
}
}
}
cout<<dp[n][m]<<endl;//輸出最優解
print_path(n,m);//打印公關子序列
cout<<endl;
}
return 0;
}
3.最大字段和
#include<iostream>
#include<cmath>
using namespace std;
int n;
int a[1005],dp[1005];
int main(){
while(1){
cin>>n;
int Max=-1;
if(n==0) break;
for(int i=0;i<n;i++) cin>>a[i];
if(a[0]<0) dp[0]=0;
else dp[0]=a[0];//處理第一個數
for(int i=1;i<n;i++) dp[i]=max(dp[i-1]+a[i],a[i]);//暫存
for(int i=0;i<n;i++) Max=max(Max,dp[i]);
cout<<"最大字段和是 "<<Max<<endl;
}
return 0;
}
4.圖像壓縮
5.0-1揹包問題
6.最優二叉搜索樹
三.貪心