# APCS實作題2024年1月第4題:合併成本
> 日期:2024年8月17日
> 作者:王一哲
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=m934)
## 題目
### 問題描述
有 $n$ 個數字排成一列,依序是 $a_1, a_2, \dots, a_n$。
每次可以挑選兩個相鄰的數字 $(u, v)$ 合併,合併會花費 $|u - v|$ 的元,合併起來的數字會變為 $u+v$。
問把 $n$ 個東西合成一個數字的最小花費是多少?
<br />
### 輸入說明
第一行有一個正整數 $n~(1 \leq n \leq 100)$,表示有多少個東西。
第二行包含 $n$ 個整數 $a_1, a_2, \dots, a_n ~(|a_i| \leq 1000)$,相鄰兩個數字之間用空格隔開。
子題分數:
- 30%:$n \leq 13$
- 70%:無額外限制
<br />
### 輸出格式
輸出最小花費。
<br />
### 範例輸入1
```
4
3 -1 2 5
```
### 範例輸出1
```
5
```
### 範例輸入2
```
6
-5 3 0 -4 3 -2
```
### 範例輸出2
```
18
```
### 範例輸入3
```
7
-1 -6 6 -8 7 0 -9
```
### 範例輸出3
```
36
```
<br /><br />
## Python 程式碼
其中第15行,計算 [i, j] 區間成本的算法分成3段,第1段用遞迴計算 [i, k] 區間成本,第2段用遞迴計算 [k+1, j] 區間成本,第3段計算 [i, k] 及 [k+1, j] 兩區加總後相減的絕對值,即
$$
|rsum - lsum| = |(tot - lsum) - lsum| = |tot - 2 \times lsum|
$$
費時最久約為 0.2 s,使用記憶體最多約為 3.6 MB,通過測試。
```python=
n = int(input()) # 讀取資料數量 n
a = list(map(int, input().split())) # 讀取資料
psum = [0]*(n+1) # 前綴和
for i in range(1, n+1): psum[i] += psum[i-1] + a[i-1]
dp = [[-1]*(n+1) for _ in range(n+1)] # dp,記錄合併 [i][j] 區間最小的成本,預設為 -1
for i in range(n+1): dp[i][i] = 0 # dp[i][i] 為同一個數字,不用合併,成本為0
def solve(i, j): # 找 [i, j] 區間合併的最小成本
if dp[i][j] != -1: return dp[i][j] # 如果不是 -1,已找過,直接回傳值
imin = 1000000 # 最低成本,預設為超出題目範圍的值
tot = psum[j] - psum[i-1] # [i, j] 區間的和
#print("i = {:d}, j = {:d}, tot = {:d}".format(i, j, tot)) # 除錯用
for k in range(i, j): # 掃過 [i, j] 區間,k 不包含 j
lsum = psum[k] - psum[i-1] # [i, k] 區間和
tmp = solve(i, k) + solve(k+1, j) + abs(tot - lsum - lsum) # 遞迴,找 [i, k] 及 [k+1, j],再加上這個區的成本
if tmp < imin: imin = tmp # 如果 tmp 小於 imin,更新 imin
dp[i][j] = imin # 存入 dp
return imin # 回傳 imin
ans = solve(1, n) # 代入 [1, n] 求答案
print(ans) # 印出答案
```
<br />
如果將串列 a 直接轉為前綴和,理論上可以再少用一點記憶體,但是這題的資料量不大,看不出效果。費時最久約為 0.2 s,使用記憶體最多約為 3.6 MB,通過測試。
```python=
n = int(input()) # 讀取資料數量 n
a = [0] # 儲存資料用的串列,之後會轉為前綴和
a += list(map(int, input().split())) # 讀取資料
for i in range(1, n+1): a[i] += a[i-1]
dp = [[-1]*(n+1) for _ in range(n+1)] # dp,記錄合併 [i][j] 區間最小的成本,預設為 -1
for i in range(n+1): dp[i][i] = 0 # dp[i][i] 為同一個數字,不用合併,成本為0
def solve(i, j): # 找 [i, j] 區間合併的最小成本
if dp[i][j] != -1: return dp[i][j] # 如果不是 -1,已找過,直接回傳值
imin = 1000000 # 最低成本,預設為超出題目範圍的值
tot = a[j] - a[i-1] # [i, j] 區間的和
#print("i = {:d}, j = {:d}, tot = {:d}".format(i, j, tot)) # 除錯用
for k in range(i, j): # 掃過 [i, j] 區間,k 不包含 j
lsum = a[k] - a[i-1] # [i, k] 區間和
tmp = solve(i, k) + solve(k+1, j) + abs(tot - lsum - lsum) # 遞迴,找 [i, k] 及 [k+1, j],再加上這個區的成本
if tmp < imin: imin = tmp # 如果 tmp 小於 imin,更新 imin
dp[i][j] = imin # 存入 dp
return imin # 回傳 imin
ans = solve(1, n) # 代入 [1, n] 求答案
print(ans) # 印出答案
```
<br /><br />
## C++ 程式碼
費時最久約為 51 ms,使用記憶體最多約為 488 kB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int solve(int i, int j, vector<vector<int>>& dp, vector<int> a) { // 找 [i, j] 區間合併的最小成本
if (dp[i][j] != -1) return dp[i][j]; // 如果不是 -1,已找過,直接回傳值
int imin = 1000000; // 最低成本,預設為超出題目範圍的值
int tot = a[j] - a[i-1]; // [i, j] 區間的和
for(int k=i; k<j; k++) { // 掃過 [i, j] 區間,k 不包含 j-1
int lsum = a[k] - a[i-1]; // [i, k] 區間和
int tmp = solve(i, k, dp, a) + solve(k+1, j, dp, a) + abs(tot - lsum - lsum); // 遞迴,找 [i, k] 及 [k+1, j],再加上這個區的成本
if (tmp < imin) imin = tmp; // 如果 tmp 小於 imin,更新 imin
}
dp[i][j] = imin; // 存入 dp
return imin; // 回傳 imin
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n; cin >> n; // 讀取資料數量 n
vector<int> a (n+1, 0); // 儲存資料用的串列,轉為前綴和
for(int i=1; i<=n; i++) {
int tmp; cin >> tmp;
a[i] = a[i-1] + tmp;
}
vector<vector<int>> dp (n+1, vector<int> (n+1, -1)); // dp,記錄合併 [i][j] 區間最小的成本,預設為 -1
for(int i=0; i<=n; i++) dp[i][i] = 0; // dp[i][i] 為同一個數字,不用合併,成本為0
int ans = solve(1, n, dp, a); // 代入 [1, n] 求答案
cout << ans << "\n"; // 印出答案
return 0;
}
```
<br /><br />
---
###### tags:`APCS`、`Python`、`C++`