本文最后更新于 2024年10月30日 下午
问题描述
设有一个长度为L L L 的钢条,在钢条上标有n n n 个位置点( p 1 , p 2 , … , p n ) (p_1,p_2,\dots,p_n) ( p 1 , p 2 , … , p n ) 。现在需要按钢条上标注的位置将钢条切割为n + 1 n+1 n + 1 段,假定每次切割所需要的代价与所切割的钢条长度成正比。请编写一个算法,能够确定一个切割方案,使切割的总代价最小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <string> #include <cmath> #include <vector> #include <algorithm> using namespace std;void MinCost (int L,int n,int *p) { }int main () { int L, n; cin>>L>>n; int *p; p = new int [n+2 ]; p[0 ] = 0 ; p[n+1 ] = L; for (int i=1 ;i<n+1 ;i++){ cin>>p[i]; } MinCost (L,n,p); return 0 ; }
样例输入:
样例输出:
问题分析
定义子问题:
考虑在切割点 p [ i ] p[i] p [ i ] 和 p [ j ] p[j] p [ j ] 之间的最小切割成本。我们可以将这个问题拆分为更小的子问题,即在 p [ i ] p[i] p [ i ] 和 p [ j ] p[j] p [ j ] 之间进行切割。
切割点的选择:
在 p [ i ] p[i] p [ i ] 和 p [ j ] p[j] p [ j ] 之间的切割点 k k k 可以是任何 p [ k ] p[k] p [ k ] ,其中 i < k < j i < k < j i < k < j 。根据选择的切割点,我们可以把区间分成两部分:
左半部分:从 p [ i ] p[i] p [ i ] 到 p [ k ] p[k] p [ k ] ,其最小切割成本为 d p [ i ] [ k ] dp[i][k] d p [ i ] [ k ] 。
右半部分:从 p [ k ] p[k] p [ k ] 到 p [ j ] p[j] p [ j ] ,其最小切割成本为 d p [ k ] [ j ] dp[k][j] d p [ k ] [ j ] 。
此时,整个区间的成本包括切割点 k k k 的成本,即 p [ j ] − p [ i ] p[j] - p[i] p [ j ] − p [ i ] 。
状态转移方程:
因此,最小成本的递归关系可以表示为:
d p [ i ] [ j ] = min i < k < j ( d p [ i ] [ k ] + d p [ k ] [ j ] + ( p [ j ] − p [ i ] ) ) dp[i][j] = \min_{i < k < j} \left( dp[i][k] + dp[k][j] + (p[j] - p[i]) \right)
d p [ i ] [ j ] = i < k < j min ( d p [ i ] [ k ] + d p [ k ] [ j ] + ( p [ j ] − p [ i ]) )
于是这个问题可以不断分解,直到子问题的规模足够小(例如,当 i + 1 = j i + 1 = j i + 1 = j 时),此时没有需要切割的区间,成本为 0。因此这个问题具有 最优子结构 的性质,可以使用动态规划法求解。
算法设计
算法步骤
初始化与排序:
首先,将切割点数组 p p p 中的切割点按位置顺序重排,并在头尾增加 0 和 L L L ,以便更好地表示切割区间。
建立动态规划表:
创建一个二维数组 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] ,并将其元素全部初始化为0,其中 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] 表示在切割点 p [ i ] p[i] p [ i ] 和 p [ j ] p[j] p [ j ] 之间的最小切割成本。
填充动态规划表:
a) 从小到大遍历切割段的长度。对于从 i i i 到 j j j 的这段,尝试所有可能的切割点 k k k (i < k < j i<k<j i < k < j )。
b) 对于每一个可能的切割点 k k k ,计算切割成本,并选择最小的值更新 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] 。
返回最终结果:
d p [ 0 ] [ n + 1 ] dp[0][n + 1] d p [ 0 ] [ n + 1 ] 就是整条钢条的最小切割成本。
复杂度分析
时间复杂度分析
对n n n 个分割点重排,最多O ( n 2 ) O(n^2) O ( n 2 ) 。
建立dp数组的时间复杂度为 O ( n 3 ) O(n^3) O ( n 3 ) 。
a) 外层循环遍历所有长度 l e n len l e n ,为 O ( n ) O(n) O ( n ) 。
b) 内层两个循环遍历所有的切割点左端点 i i i ,也为 O ( n ) O(n) O ( n ) 。
c) 对于每一对 i , j i,j i , j ,内层循环遍历所有可能的切割点 k k k ,最多 O ( n ) O(n) O ( n ) 。
于是,总体的时间复杂度为 O ( n 3 ) O(n^3) O ( n 3 )
空间复杂度分析
上述算法使用了一个大小为 O ( n 2 ) O(n^2) O ( n 2 ) 的动态规划表 d p dp d p ,所以空间复杂度为 O ( n 2 ) O(n^2) O ( n 2 ) 。
算法实现
定义类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <vector> #include <algorithm> #include <climits> using namespace std;void MinCost (int L, int n, int *p) { sort (p + 1 , p + n + 1 ); p[0 ] = 0 ; p[n + 1 ] = L; vector<vector<int >> dp (n + 2 , vector <int >(n + 2 , 0 )); for (int len = 2 ; len <= n + 1 ; len++) { for (int i = 0 ; i + len <= n + 1 ; i++) { int j = i + len; dp[i][j] = INT_MAX; for (int k = i + 1 ; k < j; k++) dp[i][j] = min (dp[i][j], dp[i][k] + dp[k][j] + p[j] - p[i]); } } cout << dp[0 ][n + 1 ] << endl; }
主函数
1 2 3 4 5 6 7 8 9 10 11 12 int main () { int L = 7 , n = 4 ; int *p = new int [n + 2 ]; p[0 ] = 1 ; p[1 ] = 3 ; p[2 ] = 4 ; p[3 ] = 5 ; MinCost (L, n, p); delete [] p; return 0 ; }
运行结果