1. 程式人生 > >openjudge:4152——從組合數生成到動態規劃的思考

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)

執行結果

所以說,演算法的重要性,是要遠高於細枝末節的。動態規劃這種高效演算法的使用,能夠更加有效地解決問題。當然,它也不是完美的,至少,又難理解又難程式設計………………