基礎動態規劃-POJ2229-計數問題

基礎的動態規劃

原理

動態規劃的思想的是把一個問題分解爲規模小一點的同樣的問題,然後找出小規模的問題和大規模問題的聯繫,從而遞歸地得出大規模問題的解。

實現方法

1.記憶化搜索(遞歸的思想)

從動態規劃的思想上理解,通過搜索的方式,將大問題分解爲小參數的性質相同的問題,但是一些不同參數的大問題可能會包含一些相同參數的小問題,對於這些小問題,我們搜索兩次太費時間,搜索完一次後,就將結果存下來,這就是所謂的記憶化搜索。

2.遞推關係式

就是直接列出大問題和小問題的聯繫,然後從小規模問題出發,組合小規模的問題得出大規模的問題。

3.例子

兩種形式是等價的,比如最簡單的求斐波那契數列。

//看這是從大規模的問題n開始入手的。記憶化搜索。

dp[n+1]
int f(int n){
    if(有保存的結果) return dp[n];
    return f(n-1)+f(n-2)}
//這是從小規模(i=3)問題開始逐漸往上推的。
for(int i=3;i<n;i++)
dp[i]=dp[i-1]+dp[i-2]

步驟

  1. 邊界條件
  2. 遞推關係式

有關計數的動態規劃問題

重集的組合問題,比如經典的找錢問題,用多種面值的鈔票,湊一個數額的錢,每種鈔票都有充分多張。

POJ 2229

Description

Farmer John commanded his cows to search for different sets of numbers that sum to a given number. The cows use only numbers that are an integer power of 2. Here are the possible sets of numbers that sum to 7:

  1. 1+1+1+1+1+1+1
  2. 1+1+1+1+1+2
  3. 1+1+1+2+2
  4. 1+1+1+4
  5. 1+2+2+2
  6. 1+2+4

Help FJ count all possible representations for a given integer N (1 <= N <= 1,000,000).
Input

A single line with a single integer, N.
Output

The number of ways to represent N as the indicated sum. Due to the potential huge size of this number, print only last 9 digits (in base 10 representation).

解題思路

首先看題意,是想用248162i2,4,8,16…2^i這樣的數湊一個整數。有多少種湊法。

錯誤的思路

是不是可以定義sum[n]爲湊成整數n的方式的個數。
自然想到sum[n]=sum[n1]+sum[n2]+sum[n4]++sum[n2i]sum[n]=sum[n-1]+sum[n-2]+sum[n-4]+…+sum[n-2^i]
解決了嗎,仔細想想看,不對這樣有重複,
比如說sum[n1]sum[n2]sum[n-1]和sum[n-2],前一個說明我們至少用了1一次,第二個說明我們至少用2一次,顯然他們不是互斥事件,相加有可能有重複。

這樣的纔是互斥,如至少用1一次和不用1。

正確的思路

顯然我們通過一個參數搞不定,這種情況一般就需要再添加一個參數,重新劃分問題,思考一哈,我們可以定義sum[i][n]指的是在前i箇中即20,212i2^0,2^1…2^i,湊成n的方法數。
這樣我們就可以用上面提到的互斥的思路來對這個問題做一個劃分。

思考這個遞推關係時,sum[i][n]sum[i][n]
比如我們想說用對於第i個數即2i2^i,這個起碼包含,我們絕對不用第i個數時的種類數即sum[i1][n]sum[i-1][n],還有就是我們至少用了一次第i個數,這相當於在前i個數中,任意挑選,湊成n2in-2^i的方法數(其實就是用了那次把那次的值減去) sum[i][n2i]sum[i][n-2^i]這樣遞歸關係式就準備好了。

sum[i][n]=sum[i1][n]+sum[i][n2i]sum[i][n]=sum[i-1][n]+sum[i][n-2^i]

邊界條件

顯然就是我們要湊的數爲0時,方法就一個。
然後就是當我們只用1時來湊任何數方法也就1個。

實現

#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
#include <cmath>
//POJ Sumsets
int main(){
    ll num;
    cin>>num;
    //因爲1000,000在2^20以內。
    //我們可以通過求N在2^1-2^20。
    //float x=log2(num); 是不是精準呢。lower_bound()
    int x=2;
    //特例
    if(num<x) return 1;
    //
    int i;
    for(i=1;i<=20;i++){
        if(num<=x) break;
        x=x*2;
    }
    i-=1;
    
    //初始化邊界條件
    vector<vector<int>> dp(i+1,vector<int>(num+1,0));
    for(int j=1;j<=num;j++){
        dp[0][j]=1;
    }
    for(int k=0;k<=i;k++){
        dp[k][0]=1;
    }
    int mod=1e9;
    
    //遞推式
    //dp[k][j]=dp[k-1][j]+dp[k][j-2^k]
    for(int k=1;k<=i;k++){
        for(int j=1;j<=num;j++){
            if((j-(1<<k))>=0)
                dp[k][j]=(dp[k-1][j]+dp[k][j-(1<<k)])%mod;
            else
                dp[k][j]=dp[k-1][j]%mod;
        }
    }
    cout<<dp[i][num]<<endl;
    return 0;
}

小tip:可以從組合數學所講母函數的角度去思考。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章