【題目地址】HDU1133
【題目大意】有m+n個人在買票,其中m個人拿着50元,n個人拿着100元,每張票50元,售票點沒有零錢可以找,輸入m,n,問有多少種方式可以使每個人都合理的買到票。
【題目分析】首先,如果m < n,肯定不行(輸出0種方案)。
其他情況下:由於m個人拿的都是50元,所以m個人之見可以隨便互換位置(有m!種排列),類似的,其餘n個人也可以隨便互換位置,有n!中排列。持有50元的人和持有100元的人之間有的可以互換位置,有的不可以,那麼下面我們就說一下哪些人之見可以互換。假設持有50元的人都是A,持有100元的人都是B。那麼排列的第一個元素一定是A,再接着可能是A,也可能是B.......這樣我們可以找到遞歸的規律,然後寫出遞歸函數。
long long temp(int one,int two,int one_sum,int two_sum) //四個參數依次表示:隊列中已經有one人持有50元
//隊列中已經有two人持有100元
//持有50元和100元的總人數
{
if(one_sum < two_sum) //持有50元總人數比持有100元的總人數還少,肯定不行,直接返回0
return 0;
if(one < two) //隊列中持有50元的人的數量比持有100元的人的數量少了,肯定不行,返回0
return 0;
if(one == one_sum || two == two_sum) //完成一種分配
return 1;
return temp(one+1,two,one_sum,two_sum) + temp(one,two+1,one_sum,two_sum); //在隊列中再多分配一個持有50元的或持有100元的人
}
上面的遞歸函數註釋已經寫的比較明白了,這裏我就不再多解釋了。遞歸函數寫起來是比較簡單的,但是這樣做的話會嚴重超時!
下面讓我們把遞歸函數改寫成for循環。通過數學上的找規律可以發現(其實當時我也找了好半天才突然之間發現的)如下規律:
1 | 1 | 1 | 1 | 1 | 1 |
0 | 1 | 2 | 3 | 4 | 5 |
0 | 0 | 2 | 5 | 9 | 14 |
0 | 0 | 0 | 5 | 14 | 28 |
long long dp[105][105];
int DP(int one_sum,int two_sum)
{
mem(dp);<span style="white-space:pre"> </span>//這裏是memset函數將dp數組初始化成0
for(int i = 0; i <= one_sum; i++)
dp[0][i] = 1ll;
for(int i = 1; i <= two_sum; i++){
for(int j = i;j <= one_sum; j++){
for(int k = 0; k <= i; k++){
dp[i][j] += dp[k][j-1];
}
}
}
return 1;
}
調用的時候只需在main函數裏面輸入: DP(m,n);
ans = dp[n][m];
這兩行即可調用。就在剛剛我寫着一篇博客畫表格的時候突然之間又發現了一個O(n^2)複雜度的求dp數組的算法(代碼如下):
int DP(int one_sum,int two_sum)
{
mem(dp);
for(int i = 0; i <= one_sum; i++)
dp[0][i] = 1;
for(int i = 1; i <= two_sum; i++){
for(int j = i; j <= one_sum; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
}
其實自己想一想很容易理解的,第一個DP做了很多重複的運算。這樣寫的話時間複雜度就大大地降了下來,這樣做的話,就不會超時了。還有一個問題就是數據太大了,要用到大數進行操作,下面將完整的java代碼附上:
import java.math.BigInteger;
import java.util.Scanner;
import javax.management.MXBean;
import javax.swing.SpringLayout.Constraints;
public class Main {
static int MAXN = 105;
static BigInteger dp[][] = new BigInteger[MAXN][MAXN];
public static void init() //剛纔的DP函數
//public void init()
{
for(int i = 1; i < MAXN; i++)
for(int j = 0; j < MAXN; j++)
dp[i][j] = BigInteger.valueOf(0);
for(int i = 0; i < MAXN; i++)
dp[0][i] = BigInteger.valueOf(1);
for(int i = 1; i < MAXN; i++){
for(int j = i;j < MAXN; j++){
dp[i][j] = dp[i-1][j].add(dp[i][j-1]);
// for(int k = 0; k <= i; k++){
// dp[i][j] = dp[i][j].add(dp[k][j-1]);
// }
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner in = new Scanner(System.in);
int cas = 0;
init(); //爲了降低時間複雜度,只需在處理數據之前初始化一下數組dp就可以了,以後每次就可以直接用dp[n][m]
while(true){
int m,n;
m = in.nextInt();
n = in.nextInt();
if(m + n == 0)
break;
System.out.println("Test #" + ++cas +":");
if(n > m){
System.out.println("0");
continue;
}
BigInteger a = BigInteger.valueOf(1);
BigInteger b = BigInteger.valueOf(1);
for(int i = 1; i <= m; i++){
a = a.multiply(BigInteger.valueOf(i));
}
for(int i = 1; i <= n; i++){
b = b.multiply(BigInteger.valueOf(i));
}
BigInteger ans;
ans = a.multiply(b);
ans = ans.multiply(dp[n][m]);
// ans = dp[n][m];
System.out.println(ans);
}
}
}
【回想一下】這道題目運用到了排列組合、java大數(當然你也可以自己用C++或java寫大數的這些操作)、數學遞推等知識。
首先要將問題分解成 n! 、 m!和 ans 的乘積,而階乘操作很好做,剩下的就是運用數學知識來找規律了,用遞歸函數計算一些簡單的值可以來幫助手算找規律,只要找到了這個規律,剩下的就簡單了,只需將C++代碼改寫成java代碼,並運用java的大數類就OK了!