你必須知道的基礎演算法

yoylee_web發表於2016-07-07

基礎演算法簡介

(1).貪心演算法

對於貪心演算法,我們要先將問題簡化,然後依據貪心演算法的理念,例如可以一起進行的事情,讓他們一起進行。可以用一個條件完成的,就用一個條件完成。貪心演算法就像人的貪心理念一樣,先將可以貪的貪乾淨,然後在考慮特殊的情況,這樣可以很好地進行程式碼的編寫。

貪心演算法也叫做貪婪演算法是一種對某些求最優解問題的更簡單、更迅速的設計技術。用貪婪法設計演算法的特點是一步一步地進行,常以當前情況為基礎根據某個優化測度作最優選擇,而不考慮各種可能的整體情況,它省去了為找最優解要窮盡所有可能而必須耗費的大量時間,它採用自頂向下,以迭代的方法做出相繼的貪心選擇,每做一次貪心選擇就將所求問題簡化為一個規模更小的子問題,通過每一步貪心選擇,可得到問題的一個最優解,雖然每一步上都要保證能獲得區域性最優解,但由此產生的全域性解有時不一定是最優的,所以貪婪法不要回溯。

下面給大家分析一個經典例題:田忌賽馬問題。

簡單題意為: 田忌和主公賽馬,輸的一方要付200錢給贏方,已知各個馬匹的速度,並且兩人所擁有的馬匹相同,求田忌所能贏得的錢數。

思路形成過程: 先將馬匹的速度從大到小排列起來,根據貪心思想,我們需要先對最小值進行比較,如果田忌的馬匹的速度大,則勝場加一。反之負場加一,若速度相同,在對最大值進行比較如果田忌的馬匹的速度大,則勝場加一。反之負場加一。最後將勝場減去負場乘200輸出即可。

程式碼如下:

#include<iostream> 
#include<algorithm> 

using namespace std; 
bool cmp(int a,int b) 
{ 
   return a>b;  
} 
int main()    
{ 
   int n,i,j,win,lost; 
   int a[10000]; 
   int b[10000]; 
   while(cin>>n&&n) 
   { 
        for(i=0;i<n;i++) 
           cin>>a[i]; 
        for(i=0;i<n;i++)   
           cin>>b[i];   
        sort(a,a+n,cmp);//將馬匹的速度從大到小排列 
        sort(b,b+n,cmp);//同上 
        win=0; 
        lost=0; 
        inttmax=0,tmin=n-1,kmax=0,kmin=n-1; 
        while(tmax<=tmin) 
        { 
            if(a[tmin]>b[kmin])//先從最小的速度進行比較 
            { 
               win++; 
                tmin--; 
                kmin--; 
            } 
            else if(a[tmin]<b[kmin]) 
            { 
                lost++; 
                tmin--; 
                kmax++; 
            } 
            else 
            { 
                if(a[tmax]>b[kmax])//如果最小速度相同則進行最大值進行比較 
                { 
                    win++; 
                    tmax++; 
                    kmax++; 
                } 
                else 
                { 
                    if(a[tmin]<b[kmax]) 
                        lost++; 
                    tmin--; 
                    kmax++; 
                } 
           } 
        } 
       cout<<(win-lost)*200<<endl; 
   } 
}

(2)二分演算法與三分演算法

二分演算法相對於三分演算法來說簡單一點,下面我們先來看看這個二分演算法。

對於二分演算法也叫做二分查詢,二分查詢又稱折半查詢,優點是比較次數少,查詢速度快,平均效能好;其缺點是要求待查表為有序表,且插入刪除困難。因此,折半查詢方法適用於不經常變動而查詢頻繁的有序列表。首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查詢關鍵字比較,如果兩者相等,則查詢成功;否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查詢關鍵字,則進一步查詢前一子表,否則進一步查詢後一子表。重複以上過程,直到找到滿足條件的記錄,使查詢成功,或直到子表不存在為止,此時查詢不成功。

二分演算法的數學模型就是單調函式求零點。下面為大家介紹一個多人分披薩例題:

簡單題意:有多個面積不相等的披薩,分給多個人,要求每個人擁有的披薩面積相同,並且不能重組。先輸入x和y為披薩和人的個數,之後輸入x個披薩的半徑,求每個人所能擁有的最大面積。

思路形成:採用二分法,先記錄所有面積的總和作為最大值max,最小值為零min=0,求中間值mid,然後將所有的披薩計算這個面積所能滿足的人數和真實人數作比較,之後再用二分法的基本方法計算即可。(其中最重要的是當計算滿足的人數時,應該將人數型別設為整形)。

程式碼如下:

#include<iostream> 
#include <cstdio>
#include<algorithm> 
using namespace std; 
double pai=acos(-1.0); 
int main() 
{ 
   int n,m,f,i,p,pp; 
   double k,size[10000],low,hight,sum,mid; 
   cin>>n; 
   while(n--) 
   { 
        sum=0; 
        cin>>m>>f; 
        for(i=1;i<=m;i++) 
        { 
            cin>>k; 
            size[i]=k*k*pai; 
            sum+=size[i]; 
        } 
        i=0; low=0; hight=sum/f;   p=0; 
        while(1) 
        { 
            p=0; 
            mid=(low+hight)/2;  //二分法中間值
            for(i=1;i<=m;i++) 
            { 
                pp=size[i]/mid;//將pp設為整形 
                p+=pp; 
            } 
            if(p>f) 
                low=mid;  //二分法的 基本程式碼
            else 
                hight=mid; 
            if((hight-low)<0.00001) 
            { 
                mid=(hight+low)/2; 
               cout<<fixed<<setprecision(4)<<mid<<endl; 
                break; 
            } 
        } 
   }     

}

二分法的基本程式碼:

mid=(low+hight)/2; if(p>f) 
                  low=mid;
                  hight=mid; 
                  if((hight-low)<0.00001) 
                  { 
                     mid=(hight+low)/2; 
                     cout<<    <<endl; 
                     break; 
                   } 

對於三分演算法就是解決凸形或者凹形函式的極值的方法,mid = (Left + Right) / 2

midmid = (mid + Right) / 2如果mid靠近極值點,則Right = midmid;否則(即midmid靠近極值點),則Left = mid。

三分法的模板如下:

doublecal(Type a)
{
    /* 根據題目的意思計算 */
}
voidsolve()
{
    double Left, Right;
    double mid, midmid;
    double mid_value, midmid_value;
    Left = MIN; Right = MAX;
    while (Left + EPS <= Right{
        mid = (Left + Right) / 2;
        midmid = (mid + Right) / 2;
        if (cal(mid)>=cal(midmid))

            Right = midmid;
        else Left = mid; }
}

三分法應用的不多,就不再舉例題了。

(3)搜尋演算法:DFS和BFS

DFS:深度優先搜尋演算法,正如演算法名稱那樣,深度優先搜尋所遵循的搜尋策略是儘可能“深”地搜尋圖。在深度優先搜尋中,對於最新發現的頂點,如果它還有以此為起點而未探測到的邊,就沿此邊繼續漢下去。當結點v的所有邊都己被探尋過,搜尋將回溯到發現結點v有那條邊的始結點。這一過程一直進行到已發現從源結點可達的所有結點為止。如果還存在未被發現的結點,則選擇其中一個作為源結點並重復以上過程,整個程式反覆進行直到所有結點都被發現為止。

BFS:廣度優先搜尋演算法,是最簡便的圖的搜尋演算法之一,這一演算法也是很多重要的圖的演算法的原型。Dijkstra單源最短路徑演算法和Prim最小生成樹演算法(後續有介紹)都採用了和寬度優先搜尋類似的思想。他屬於一種盲目搜尋法,目的是系統地展開並檢查圖中的所有節點,以找尋結果。換句話說,它並不考慮結果的可能位置,徹底地搜尋整張圖,直到找到結果為止。它之所以稱之為寬度優先演算法,是因為演算法自始至終一直通過已找到和未找到頂點之間的邊界向外擴充套件,就是說,演算法首先搜尋和s距離為k的所有頂點,然後再去搜尋和S距離為k+l的其他頂點。

下面介紹連連看典型BFS例題:

簡單題意:規則是在一個棋盤中,放了很多的棋子。如果某兩個相同的棋子,可以通過一條線連起來(這條線不能經過其它棋子),而且線的轉折次數不超過兩次,那麼這兩個棋子就可以在棋盤上消去。連線不能從外面繞過去的,玩家滑鼠先後點選兩塊棋子,試圖將他們消去,然後遊戲的後臺判斷這兩個方格能不能消去。

思路分析:首先明確這是一個搜尋的問題,因為只要找到解決的方法即可,就考慮用廣度搜尋,建立一個佇列,用方向陣列將方向記錄下來。在運用廣度搜尋的辦法將每一級的所有可能情況壓入佇列,在判斷是否到達最終條件即可。

程式碼如下: 

#include <iostream> 
#include<queue> 
#include<cstring> 
usingnamespace std; 
structnode  { 
    int x, y; 
    int t, d; }; 
queue<node>q; 
intn, m, map[1002][1002], prove; 
intvisit[1002][1002][4];  
intqry, sx, sy, ex, ey; 
intdx[4] = {0, -1, 0, 1}; 
intdy[4] = {1, 0, -1, 0}; 
intcheck(int x, int y)  { 
    if(x < 1 || x > n || y < 1 || y> m)//方向陣列記錄方向 
        return 0; 
    else 
        return 1; } 
voidbfs()  { 
    while(!q.empty()) 
        q.pop(); 
    memset(visit, 0, sizeof(visit)); 
    node s, e; 
    s.x = sx; s.y = sy; 
    s.t = 0;  s.d = -1; 
    q.push(s); 
    while(!q.empty())  { 
        s = q.front(); 
        q.pop(); 
        if(s.t > 2) 
            continue; 
        if(s.x == ex && s.y == ey)//最終成立的條件    { 
            prove = 1; 
            cout << "YES"<< endl; 
            break;    } 
        for(int i = 0; i < 4; i++)  { 
            e.x = s.x + dx[i];  e.y = s.y + dy[i]; 
            if(!check(e.x, e.y) ||visit[s.x][s.y][i])   
                continue; 
            if( map[e.x][e.y] == 0 || (e.x ==ex && e.y == ey) )   { 
                if(s.d == -1 || i == s.d)   { 
                    e.d = i; 
                    e.t = s.t; 
                    q.push(e); 
                    visit[s.x][s.y][i] = 1;     } 
                else   { 
                    e.d = i; 
                    e.t = s.t + 1; 
                    q.push(e); 
                   visit[s.x][s.y][i] = 1; 
                }    }  }   }  } 
intmain()  { 
    while(cin >> n >> m)  { 
        if(!n && !m) 
            break; 
            int i=1; 
            int j=1; 
        for(i = 1; i <= n; i++) 
        for( j = 1; j <= m; j++) 
            cin >> map[i][j]; 
        cin >> qry; 
        for(i=1; i<= qry;i++)   { 
            cin >> sx >> sy>> ex >> ey; 
            prove = 0; 
            if(map[sx][sy] == map[ex][ey]&& map[sx][sy] != 0) 
               bfs(); 

            if(!prove) 
                 cout << "NO" <<endl;   }   }  }

(4).動態規劃以及動態規劃中的揹包問題

動態規劃演算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規劃演算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。如果我們能夠儲存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重複計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。具體的動態規劃演算法多種多樣,但它們具有相同的填表格式。

動態規劃中的揹包問題之01揹包問題:

講述便從一個最經典的例題開始吧: 某商店有n個物品,第i個物品價值為vi,重量(或稱權值)為wi,其中vi和wi為非負數, 揹包的容量W,W為一非負數。目標是如何選擇裝入揹包的物品,使裝入揹包的物品總價值最大,所選商品的一個可行解即所選商品的序列如何?揹包問題與0-1揹包問題的不同點在於,在選擇物品裝入揹包時,可以只選擇物品的一部分,而不一定要選擇物品的全部。

簡單題意:給定n和物品和一人揹包,物品i的重量是wi,其價值為vi,問如何選擇裝入揹包的物品,使得裝入揹包的物品的總價值最大?

思路形成:在程式碼中體現

程式碼如下: 

#include<stdio.h>  
int c[10][100];/*對應每種情況的最大價值*/  
int knapsack(int m,int n)  {  
int i,j,w[10],p[10],x[10];  
for(i=1;i<n+1;i++)  
        scanf("\n%d,%d",&w[i],&p[i]);  
for(i=0;i<10;i++)  
      for(j=0;j<100;j++)  
           c[i][j]=0;/*初始化陣列*/  
for(i=1;i<n+1;i++)  
      for(j=1;j<m+1;j++)  
           {  
            if(w[i]<=j) /*如果當前物品的容量小於揹包容量*/  
                     {  
                      if(p[i]+c[i-1][j-w[i]]>c[i-1][j])   
                          /*如果本物品的價值加上揹包剩下的空間能放的物品的價值大於上一次選擇的最佳方案則更新c[i][j]*/  
                            c[i][j]=p[i]+c[i-1][j-w[i]];  
                            else  
                            c[i][j]=c[i-1][j];  
                     }  
              else c[i][j]=c[i-1][j];      }  
printf("揹包所能容納商品的最大價值為:%d\n",c[n][m]);  
printf("所選擇的商品的一個序列為:\n");  
for(i=n;i>=2;i--)       /*輸出所選商品序列*/  
    {  
     if (c[i][m]==c[i-1][m]) x[i]=0;  
     else x[i]=1;  
     m=m-w[i];  
     }  
x[1]=c[1][m]?1:0;   
for(i=1;i<=n;i++)  
printf("%3d",x[i]);  }    
int main()  {  
    int n,W;int i,j,s;  
    clrscr();  
    printf("輸入商品數量 n 和揹包容量W:\n");  
    scanf("%d,%d",&n,&W);  
    printf("輸入每件商品的重量,價值:\n");  
    knapsack(W,n);   
    printf("\n");   }

動態規劃之完全揹包問題列題:有N種物品和一個容量為V的揹包,每種物品都有無限件可用。第i種物品的體積是v[i],重量是w[i]。求解將哪些物品裝入揹包可使這些物品的體積總和不超過揹包容量,且重量總和最大

思路形成:01揹包中每種物品有且僅有一件,而完全揹包問題則不同,每種物品均有無限件可用。顯然,狀態轉移方程可以通過簡單地修改01揹包問題得s[i][j]=max{s[i-1][j-k*v[i]]+k*w[i]}(0<=k<=V/v[i]),遞推邊界和01揹包相同,當i=0時,s[i][j]=0。從狀態轉移方程我們不難發現,此演算法同樣有O(NV)個狀態需要求解,不同的是此演算法求解每個狀態的時間並不是O(1),所以總的時間複雜度是超過了O(NV)的。

優化:01揹包問題中,需要決策的是是否選取第i種物品,同樣,完全揹包問題也可以這樣決策。若不選取第i種物品,當前最優結果s[i][j]顯然等於s[i-1][j];若選取第i種物品,則可以由之前選取了若干個第i種物品得到的最優解的基礎上再加一個第i種物品得到,於是s[i][j]=s[i][j-v[i]]+w[i](s[i][j-v[i]]可以看作s[i-1][j-(k-1)*v[i]])。實際當前最優解是從以上兩種情況中選取更優的結果,於是得到狀態轉移方程s[i][j]=max{s[i-1][j], s[i][j-v[i]]+w[i]}。遞推邊界和之前相同,當i=0時,s[i][j]=0。

部分程式碼如下:

for(int i=0; i<=V; i++) s[0][i]=0;  // 邊界
for(int i=1; i<=N; i++)
{
      for (int j=0; j<=V; j++)
      {
            int t=0;
            if (j-v[i]>=0)t=s[i][j-v[i]]+w[i];
            s[i][j]=max(s[i-1][j], t);
      }

上面是個時空複雜度均為O(NV)的程式碼,顯然通過將狀態記錄陣列改成一維可以將空間複雜度降為O(V),具體實現見下面的程式碼:

for(int i=0; i<=V; i++) s[i]=0;  // 邊界
for(int i=1; i<=N; i++)
{
      for (int j=v[i]; j<=V; j++)s[j]=max(s[j], s[j-v[i]]+w[i]);
}

其餘的揹包問題用的不多,便不再贅述。

(5)圖演算法

先說明兩個圖的儲存方式鄰接矩陣和鄰接表,鄰接矩陣的使用場合為:資料規模不大n <= 1000,m越大越好、稠密圖最好用鄰接矩陣、圖中不能有多重邊出現。

鄰接表的基礎程式碼為 

struct edge
{
    int x, y, nxt; typec c;
}bf[E];


voidaddedge(int x, int y, typec c)
{
    bf[ne].x = x; bf[ne].y = y; bf[ne].c = c;
    bf[ne].nxt = head[x]; head[x] = ne++;
}

並查集:將編號分別為1…N的N個物件劃分為不相交集合,在每個集合中,選擇其中某個元素代表所在集合。常見兩種操作:合併兩個集合,查詢某元素屬於哪個集合。

最小生成樹問題之Prim演算法:

基本思想:任取一個頂點加入生成樹;在那些一個端點在生成樹裡,另一個端點不在生成樹裡的邊中,取權最小的邊,將它和另一個端點加進生成樹。重複上一步驟,直到所有的頂點都進入了生成樹為止。

基本內容:設G=(V,E)是連通帶權圖,V={1,2,…,n}。構造G的最小生成樹的Prim演算法的基本思想是:首先置S={1},然後,只要S是V的真子集,就作如下的貪心選擇:選取滿足條件iS,jV-S,且c[i][j]最小的邊,將頂點j新增到S中。這個過程一直進行到S=V時為止。在這個過程中選取到的所有邊恰好構成G的一棵最小生成樹。

基礎程式碼: 

int prim(int n,int mat[][MAXN],int* pre){
    int min[MAXN],ret=0;
    int v[MAXN],i,j,k;
    for (i=0;i<n;i++)
           min[i]=inf,v[i]=0,pre[i]=-1;
    for (min[j=0]=0;j<n;j++){
           for (k=-1,i=0;i<n;i++)
                  if(!v[i]&&(k==-1||min[i]<min[k]))
                         k=i;
           for(v[k]=1,ret+=min[k],i=0;i<n;i++)
                  if(!v[i]&&mat[k][i]<min[i])
                         min[i]=mat[pre[i]=k][i];
    }
    return ret;
}

最小生成樹問題之Kruskal演算法:

基本思想:將邊按權值從小到大排序後逐個判斷,如果當前的邊加入以後不會產生環,那麼就把當前邊作為生成樹的一條邊。最終得到的結果就是最小生成樹。並查集。

基本內容:把原始圖的N個節點看成N個獨立子圖;每次選取當前最短的邊,看兩端是否屬於不同的子圖;若是,加入;否則,放棄;迴圈操作該步驟二,直到有N-1條邊;一維陣列,將所有邊按從小到大的順序存在陣列裡面先把每一個物件看作是一個單元素集合,然後按一定順序將相關聯的元素所在的集合合併。能夠完成這種功能的集合就是並查集。對於並查集來說,每個集合用一棵樹表示。它支援以下操作:Union (Root1, Root2) //合併兩個集合;Findset(x) //搜尋操作(搜尋編號為x所在樹的根)。樹的每一個結點有一個指向其父結點的指標。

基礎程式碼:

void MakeSet()
{
    long i;
    for (i=0;i<=m;++i)
    {
        father[i]=i;
    }
}

longFind(long i)
{
    long r=i;
    while (father[r]!=r)
    {
        r=father[r];
    }
    while (father[i]!=r)
    {
        long j=father[i];
        father[i]=r;
        i=j;
    }
    return r;
}

 

相關文章