openjudge:4152——從組合數生成到動態規劃的思考
題目
http://bailian.openjudge.cn/practice/4152/
總時間限制: 1000ms
記憶體限制: 65536kB
描述
給定n個1到9的數字,要求在數字之間擺放m個加號(加號兩邊必須有數字),使得所得到的加法表示式的值最小,並輸出該值。例如,在1234中擺放1個加號,最好的擺法就是12+34,和為36
輸入
有不超過15組資料
每組資料兩行。第一行是整數m,表示有m個加號要放( 0<=m<=50)
第二行是若干個數字。數字總數n不超過50,且 m <= n-1
輸出
對每組資料,輸出最小加法表示式的值
樣例輸入
2 123456 1 123456 4 12345
樣例輸出
102 579 15
提示
要用到高精度計算,即用陣列來存放long long 都裝不下的大整數,並用模擬列豎式的辦法進行大整數的加法。
最初的想法
肯定python最簡單,所以決定py一波。最初想法也很簡單,主體想法就是往大數裡面的空隙加入加號就可以了。然後比較所有結果要最小的那個即可。
然後我就開始了我的python第一次嘗試:
import itertools as it def process(number, s, s_length): min = float("inf") sum = 0 for i in it.combinations(list(range(1, s_length)), number): one_combination = list(i) one_combination_len = len(one_combination) sum += int(s[0:one_combination[0]]) for j in range(0, one_combination_len - 1): sum += int(s[one_combination[j]:one_combination[j + 1]]) sum += int(s[one_combination[one_combination_len - 1]:]) if sum < min: min = sum sum = 0 print(min) if __name__ == '__main__': add_number = input() if (add_number == ''): exit(0) s = input() s_length = len(s) process(int(add_number), s, s_length) while (add_number != ''): add_number = input() if (add_number == ''): break s = input() s_length = len(s) process(int(add_number), s, s_length)
emmm,咋一看沒有問題,看看執行結果
好像也沒有什麼不對,於是興高采烈的去提交。
然而——就runtime error了。
百思不得其解,昨晚上網上隨便搞了個c++先應付一下也沒當回事。
#include<iostream> #include<string> #include<cstring> using namespace std; struct BigInt { int num[110]; int len; BigInt operator+(const BigInt &n) { //過載+,使得a+b在a,b都是BigInt變數的時候能成立 int ml = max(len, n.len); int carry = 0; //進位 BigInt result; for (int i = 0; i < ml; ++i) { result.num[i] = num[i] + n.num[i] + carry; if (result.num[i] >= 10) { carry = 1; result.num[i] -= 10; } else carry = 0; } if (carry == 1) { result.len = ml + 1; result.num[ml] = 1; } else result.len = ml; return result; } //另一種寫法 /* BigInt operator+(const BigInt &n){ int ml=max(len,n.len); int carry=0; BigInt result; for(int i=0;i<ml;i++){ result.num[i]=num[i]+n.num[i]+carry; carry=result.num[i]/10; result.num[i]=result.num[i]%10; } if(carry!=0){ result.num[ml]=carry; result.len=ml+1; }else result.len=ml; return result; } */ bool operator<(const BigInt &n) { //過載運算子小於號 if (len > n.len) return false; else if (len < n.len) return true; else { for (int i = len - 1; i >= 0; --i) { if (num[i] < n.num[i]) return true; else if (num[i] > n.num[i]) return false; } return false; } } BigInt() { //大數的無參構造方法 len = 1; memset(num, 0, sizeof(num)); } BigInt(const char *n, int L) { //由長度為L的char陣列構造大整數,n裡面的元素取值範圍從1到9。 memset(num, 0, sizeof(num)); len = L; for (int i = 0; i < len; ++i) num[len - 1 - i] = n[i] - '0'; } }; ostream &operator<<(ostream &o, const BigInt &n) //過載輸出流 { for (int i = n.len - 1; i >= 0; --i) o << n.num[i]; return o; } const int MAXN = 60; char a[MAXN]; BigInt Num[MAXN][MAXN];//Num[i][j]表示從第i個數字到第j個數字所構成的整數 BigInt V[MAXN][MAXN];//V[i][j]表示i個加號放到前j個數字中間,所能得到的最佳表示式的值 int main() { int m, n; BigInt inf; //大數定義時,直接呼叫無參構造方法 inf.num[MAXN - 2] = 1; inf.len = MAXN - 1;//無窮大 while (cin >> m) { cin >> a + 1; //從下標為1的地方開始存 n = strlen(a + 1); for (int i = 1; i <= n; ++i) for (int j = i; j <= n; ++j) Num[i][j] = BigInt(a + i, j - i + 1); //預處理 for (int j = 1; j <= n; ++j) V[0][j] = BigInt(a + 1, j); //在前j個數之前加0個加號,所形成的最小表示式的值就是這前j個數字串連成的數值 for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (j - 1 < i) V[i][j] = inf; //j個數最多加j-1個加號,如果加號的數目i>j-1,表示式都形不成,而這個肯定不是最佳表示式的最小值,所以我們初始化inf else { BigInt tmpMin = inf; for (int k = i; k < j; ++k) { //加號可放的最右邊的距離 BigInt tmp = V[i - 1][k] + Num[k + 1][j]; if (tmp < tmpMin) tmpMin = tmp; } V[i][j] = tmpMin; } } } cout << V[m][n] << endl; } return 0; }
反正他就莫名奇妙的過了,也沒多管,但是唯一讓我有點疑惑的是,他貌似沒用我生成的組合數去新增加號啥的。
今天的時候,我帶著對可能是python中大數處理不當的疑惑進行了學習,但結果告訴我python的大數處理比java不知道高到哪裡去了,更不用像上一段c++程式碼一樣,還要自己定義大數。於是我研究了一下這一段java程式碼(PS oj沒通過,但是結果沒問題,估計是結束時的輸入輸出沒處理好):
import java.math.BigInteger;
import java.util.Scanner;
public class zjh{
public static void main(String[] args) {
int m;
String s;
BigInteger n,temp,mn,inf = new BigInteger("9999999999999999999999999999999999999");
BigInteger dp[][] = new BigInteger[55][55];
Scanner in = new Scanner(System.in);
while(in.hasNext()){
m = in.nextInt();
n = in.nextBigInteger();
for(int i = 0;i < n.toString().length();i++)
dp[0][i] = new BigInteger(n.toString().substring(0, i + 1));//先初始化dp陣列
for(int i = 1;i <= m;i++){
for(int j = 0;j < n.toString().length();j++){//下標從0開始
if(i > j)
dp[i][j] = inf;
else{
mn = inf;
for(int k = i;k <= j;k++){
temp = dp[i - 1][k - 1].add(new BigInteger(n.toString().
substring(k,j + 1)));
mn = mn.min(temp);
}
dp[i][j] = mn;
}
}
}
System.out.println(dp[m][n.toString().length() - 1]);
}
}
}
前戲:
for(int i = 0;i < n.toString().length();i++)
dp[0][i] = new BigInteger(n.toString().substring(0, i + 1));//先初始化dp陣列
這裡生成的結果,就是按照不同位置開始的數字,舉個例子吧,比如大數是123456,它將生成
1、12、123、1234、12345、123456
大數啥的已經瞭解了基本用法就不再贅述了,這裡面也同樣沒有使用組合數,而是使用了演算法課我認真聽都沒聽懂的動態規劃…………關鍵程式碼——
mn = inf;
for(int k = i;k <= j;k++){
temp = dp[i - 1][k - 1].add(new BigInteger(n.toString().substring(k,j + 1)));
mn = mn.min(temp);
}
dp[i][j] = mn;
這裡實際上是動態規劃的狀態轉移方程,即
下一步的行動=(當前所有情況+對應下一步的情況)取最優解
也就是,這一行的這個數的取值,為前面所有數字的情況和該數字的取值,加上後面從原來大數中暫時取的子字串所對應數字之和,的最小值。
然後每一行(dp矩陣第一維度)得出一個最小值,從上往下,從左往右,不斷進行狀態轉移。
最終,這個矩陣,同一行,右邊比左邊小;同一列,下邊比上邊小。
而這個矩陣的右下角,是便利了所有矩陣情況的最小值,即所有+號新增位置的最小值,也就是要輸出的結果。
於是,本著python包打天下的原則,我克隆出了python原始碼,以供參考。
dp = [[0 for j in range(55)] for k in range(55)]
mn = float("inf")
# m 個數 n 大數 n_len 大數長度
def process(m, n, n_len):
for i in range(0, n_len):
dp[0][i] = int(n[0:i + 1])
for i in range(1, m + 1):
for j in range(0, n_len):
if (i > j):
dp[i][j] = float("inf")
else:
mn = float("inf")
for k in range(i, j + 1):
temp = dp[i - 1][k - 1] + int(n[k:j + 1])
mn = min(mn, temp)
dp[i][j] = mn
print(dp[m][n_len - 1])
if __name__ == '__main__':
add_number = input()
if (add_number == ''):
exit(0)
s = input()
s_length = len(s)
process(int(add_number), s, s_length)
while (add_number != ''):
add_number = input()
if (add_number == ''):
break
s = input()
s_length = len(s)
process(int(add_number), s, s_length)
執行結果
所以說,演算法的重要性,是要遠高於細枝末節的。動態規劃這種高效演算法的使用,能夠更加有效地解決問題。當然,它也不是完美的,至少,又難理解又難程式設計………………