張菖蒲點數計算--深搜(dfs)解決


這是啥?

先上鏈接

exe附源碼一份

鏈接: https://pan.baidu.com/s/1VQ6zlxCDqsGMtEuMc7bynA 提取碼: fx6z

C版本

Python版,GUI界面

https://github.com/Wfo-12/zcp

img

問題來了,標題是什么意思

(以下皆C語言版本,python版本源碼在github,上面已給出)

嚴教:簡單的說就是 拿n張牌,從這n張牌里分出兩組任意數量的牌,加起來總和相等的話就能給玩家

前幾天玩三國殺,朋友抽了個張菖蒲出來,一開始還好,就拿4張牌讓我算,差不多看一眼就出答案了

直到有一次他掏出8張牌,1分鍾的技能持續時間,我硬是一種都沒算出來。一氣之下寫了個程序來算

把這個技能轉換成算法題就是這個意思:

img-3

在游戲里追求牌多,所以同樣的點數和就只輸出了一種

算法分析

因為是寫來在游戲里用的,以輕量級為主,我這里使用C語言編寫

  1. 不多說,上來就是導入stdio
#include<stdio.h>
  1. 這題第一眼看過去就能想到是一種排列組合。不過先別急,分析一下先。要在n張牌里分出兩組牌,令第一組牌為A,第二組為B

    首先要搞清楚,並不是所有的牌都參與到分組中,也就是說可以有多余的牌,那么A與B的關系就不是簡單的A+B=n了,所以A和B兩組牌是分別進行的排列組合。

    解決排列組合最方便的算法應該就是深搜了,如果對深搜不熟悉或者沒聽過的話可以去CSDN搜索(dfs、深度優先搜索)。CSDN上有很多關於深搜的講解

  2. 既然清楚了是兩個排列組合,那么就決定使用兩個深搜。先定義幾個全局變量

    #define MAXLENTH 16
    // 全局變量
    int a[MAXLENTH] = { 0 }; // 輸入數組
    int b[MAXLENTH] = { 0 }; // 0-未出現  1-第一組  2-第二組
    int c[10000] = { 0 }; // 記錄sum
    int sum1 = 0; 
    int sum2 = 0;
    int n;
    int k = 0; // c數組下標
    int t = 1; // 深度1
    int t2 = 1; // 深度2
    
    // 函數聲明
    void dfs(int i, int x);  // 深搜-第一組
    void dfs2(int i, int x);  // 深搜-第二組
    void pt(); // 輸出
    
    • MAXLENTH → 自定義的輸入最大長度
    • a數組 → 輸入數據
    • b數組 → a數組中各個數據的狀態
    • c數組 → 已出現過的牌堆和
    • sum1 → 牌堆1的點數和
    • sum2 → 牌堆2的點數和
    • n → 輸入數據個數
    • k → 當前c數組長度
    • t / t2 → 兩個深搜函數的最大到達深度

    這里先不解釋這些變量的作用,能看懂當我沒說,我也覺得我寫的好亂,應該是那時候太急了。

  3. 先來看輸入函數pt

    void pt() {
        int x = 1;
        for (int i = 0; i < 2; i++) {
        for (int j = 0; j < n; j++) 
            if (b[j] == x) printf("%d ", a[j]);
        printf("\n");
        x++;
     }
        printf("\n");
    }
    

    為什么i是0到1? (*`д´*) 這都看不出來是輸出兩組數嗎

    j呢? j不就是遍歷數組嗎

    x是什么, x是狀態,定義變量的時候注釋了b數組的3中狀態,x就是特定的一種狀態,當x等於1的時候, 也就是i等於0的那次循環,輸出b[j]等於1的a[j],也就是第一組數據

    然后x++,在輸出第二組數

    應該不難理解,這里提一下就是先知道b[j]的3種狀態分別表示什么

  4. 接下來看看深搜部分。理解題目之后應該清楚,在例子1 2 3 4 5 6中,31 2是一對,4 62 3 5也是一對,所以兩組數各自的長度是未知的,可以是一個數、兩個數、三個數……

    那么我們每次排列組合當然需要一個深度咯

    int t = 1; // 深度1
    int t2 = 1; // 深度2
    

    開頭定義的兩個全局變量t t2分別就是兩個深搜的深度

    假設有n個數,那么開始t t2都是1,也就是只有一層深度,兩組數都是只有一個數,分別都是遍歷了整個a數組,而且不能重復

    還是看圖直觀吧

第一個深搜一個一個慢慢遍歷,深搜1每次指向一個數的時候深搜2都要跑一遍整個數組,每一次都判斷總和是否相等,當然如果某個數已經被選中了就不選了(也就是b數組對應的下標不是0了)。

否則的話就選中(更改b數組中的這個位置的數為1或者2,1就是第一組,2就是第二組)

當然圖里紅線藍線都經過了a[0],不是畫錯了,因為遍歷可不管你這個位置b是多少,只有判斷才管

像這樣藍線走完一次之后,紅色箭頭像下走一格,然后藍線繼續老操作。

是不是很簡單,但是這才第一層

如果我第二組要兩個數呢

還是畫個圖

第一組還是一個數,所以還是老的做法,紅色箭頭每指向一個新的數,綠色箭頭要依次遍歷a數組,並取出這個數來作為第二組的其中一個數,然后進行第二次深搜,也就是藍線

這么說可能有點難理解,換個形式就是

for(){
    for(){
 
    }
}

這樣的嵌套遍歷,第一個for取一個數,第二for也取一個數

這兩個不同的數共同作為第二組數

如果你不懂深搜可能要問了,那為什么不直接用for?深搜是什么?

別急,這里還只是1-2(下面都這樣表示兩組數分別的數量,左邊表示第一組的數,右邊表示第二組)

首先我們要明確一點,for循環是定死的,獲取可以用break來減少循環,但你總不能增加循環吧。

寫了4個for運行能來5個for?

其次,基數小的時候確實寫for方便,但數量一大呢,例如我開頭說的給我了8張牌,4-4甚至1-7的時候呢,寫7個循環?不現實吧。

所以我們要用深搜

直接上代碼

void dfs(int i, int x) {
    if(x == t){
       return;
    }
    while (i < n) {
       if (b[i] == 0) {
          b[i] = 1;
      sum1 += a[i];
      dfs(i + 1, x + 1);
      b[i] = 0;
      sum1 -= a[i];
   }
   i++;
    }
};

這是一個最典型的深搜,當然我還沒加一些別的條件

僅僅是對一組數排列組合

第一個if不用說了,就是深度(也就是遞歸跳出條件)

i就是當前遍歷的下標

while中如果當前下標沒被選擇那么就選中它並且表明是1(第一組),同時使這一組數的和sum1加上這個數,然后繼續調用自身進行搜索,出來之后也就是要換目標了,於是讓這個位置的b重置為0,表示未選中,再讓sum1減去這個數

這就是個簡單的排列組合,所以第二組的深搜也同樣可以調用這個模塊

void dfs(int i, int x) {
    if (x == t) {
        for (int j = 1; j <= n - t; j++) {
            sum2 = 0;
            t2 = j;
            dfs2(0, 0);
       }
   return;
}
 
// 模塊
while (i < n) {
    if (b[i] == 0) {
        b[i] = 1;
        sum1 += a[i];
        dfs(i + 1, x + 1);
        b[i] = 0;
        sum1 -= a[i];
    }
    i++;
}
};


void dfs2(int i, int x) {
    if (x == t2) {
        if (sum1 == sum2) {
            int m;
            for (m = 0; m <= k; m++) 
                if (c[m] == sum1) break;
            if (m >= k) {
                c[k] = sum1;
                k++;
                printf("第%d種\n", k);
                pt();
            }
    }
    return;
}
 
// 模塊
while (i < n) {
    if (b[i] == 0) {
        b[i] = 2;
        sum2 += a[i];
        dfs2(i + 1, x + 1);
        b[i] = 0;
        sum2 -= a[i];
    }
    i++;
}
 
}

上面我已經把相同的模塊標注出來了,要注意的是里面的部分變量要修改成對應的變量

下面來看他們不同的部分

dfs ( 第一組 ) 中:

if (x == t) {
    for (int j = 1; j <= n - t; j++) {
        sum2 = 0;
        t2 = j;
        dfs2(0, 0);
    }
    return;
}

x 等於t時也就是到達當前最大深度時執行這些代碼

for循環從1到n-tn-t就是剩下幾個數,所以第二個深搜的深度可以從1到n-t,每一次循環重置sum2並且改變t2,調用第二組的深搜。

應該不難理解,就是以第一組為主,第一組每找到一種排列組合,就要讓第二組找出他的所有排列組合情況

dfs2 ( 第二組 ) 中:

if (x == t2) {
    if (sum1 == sum2) {
        int m;
        for (m = 0; m <= k; m++) 
            if (c[m] == sum1) break;
       if (m >= k) {
            c[k] = sum1;
            k++;
            printf("第%d種\n", k);
            pt();
        }
    }
return;
}

x等於t2,第二組的深搜到達當前最大深度,要知道我們前置條件就是第一組已經到達最大深度,所以這時候就可以開始比較了。

如果兩個sum相等的話,首先當然要遍歷c數組看看有沒有這個和,如果有就直接跳過了,沒有的話繼續執行下面的代碼。

注意k是全局變量,我們不動他的時候他就是個定值,用來表示c數組的長度在合適不過了

話題回來,如果還沒有過這個和,那么就向c數組中添加這個值,並且輸出。

  1. 兩個函數部分就到這里結束了,細心的話能發現目前為止我們的第一組始終只有一個數,因為我們的t沒變過

    這個實現方法很多,我是寫在主函數中,所以放到了最后

    int main() {
        while(1){
            printf("輸入n ( 0結束 ): ");
            scanf("%d", &n);
            if (!n) break;
            printf("輸入數字序列:");
            for (int i = 0; i < n; i++) {
                scanf("%d", &a[i]);
            }
            /************************/
            for (int i = 1; i <= n; i++) {
                sum1 = 0;
                t = i;
                dfs(0, 0);
            }
            /************************/
     	// 初始化
            for (int i = 0; i <= k; i++) {
                c[i] = 0;
                k = 0;
            }
            for (int i = 0; i <= n; i++) {
                a[i] = 0;
                b[i] = 0;
            }
        }
    }
    

    *號包圍起來的部分就是不斷的改變t的值來使第一組數的數量發生變化,不多講了

    其他部分的代碼應該也不難懂。

    講解就到這了,下面附上完整代碼

完整代碼

#include<stdio.h>
int a[16] = { 0 }; // 輸入數組
int b[16] = { 0 }; // 0-未出現  1-第一組  2-第二組
int c[10000] = { 0 }; // 記錄sum
int sum1 = 0;
int sum2 = 0;
int n;
int k = 0; // c數組下標
int t = 1; // 深度1
int t2 = 1; // 深度2

void dfs(int i, int x);  // 深搜-第一組
void dfs2(int i, int x);  // 深搜-第二組
void pt(); // 輸出

int main() {
       while(1){
           printf("輸入n ( 0結束 ): ");
           scanf("%d", &n);
           if (!n) break;
           printf("輸入數字序列:");
           for (int i = 0; i < n; i++) {
               scanf("%d", &a[i]);
           }
           for (int i = 1; i <= n; i++) {
               sum1 = 0;
               t = i;
               dfs(0, 0);
           }
   		// 初始化
           for (int i = 0; i <= k; i++) {
               c[i] = 0;
               k = 0;
           }
           for (int i = 0; i <= n; i++) {
               a[i] = 0;
               b[i] = 0;
           }
       }
}
void dfs(int i, int x) {
   if (x == t) {
       for (int j = 1; j <= n - t; j++) {
           sum2 = 0;
           t2 = j;
           dfs2(0, 0);
       }
       return;
   }
   while (i < n) {
       if (b[i] == 0) {
           b[i] = 1;
           sum1 += a[i];
           dfs(i + 1, x + 1);
           b[i] = 0;
           sum1 -= a[i];
       }
       i++;
   }
};
void dfs2(int i, int x) {
    if (x == t2) {
	if (sum1 == sum2) {
	    int m;
	    for (m = 0; m <= k; m++) 
		if (c[m] == sum1) break;
	    if (m >= k) {
	        c[k] = sum1;
		k++;
		printf("第%d種\n", k);
		pt();
	    }
	}
	return;
    }
    while (i < n) {
        if (b[i] == 0) {
	    b[i] = 2;
	    sum2 += a[i];
	    dfs2(i + 1, x + 1);
	    b[i] = 0;
	    sum2 -= a[i];
	}
	i++;
    }
}
void pt() {
    int x = 1;
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < n; j++) 
		if (b[j] == x) printf("%d ", a[j]);
	    printf("\n");
	    x++;
        }
    printf("\n");
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM