⎨ ⎧ 0 , d p ( i − 1 , j − 1 ) + 1 , max { d p ( i − 1 , j ) , d p ( i , j − 1 )} , i = 0 or j = 0 i , j > 0 and x i = y j i , j > 0 and x i = y j 如果不仅想要求出 LCS 的长度,而是将 LCS 给出的话,在算法设计时还需要引入标记函数 以进行解的追踪。从状态转移方程对d p ( i , j ) dp(i,j) d p ( i , j ) 的赋值可能性中,我们可以人为在选取某种可能时顺势保存下当时的选择。
这里我们规定标记函数为:
B ( i , j ) = { ↖ , d p ( i , j ) = d p ( i − 1 , j − 1 ) + 1 ← , d p ( i , j ) = d p ( i , j − 1 ) ↑ , d p ( i , j ) = d p ( i − 1 , j ) B(i,j)=\begin{cases} ↖,&dp(i,j)=dp(i-1,j-1)+1\\ ←,&dp(i,j)=dp(i,j-1)\\ ↑,&dp(i,j)=dp(i-1,j) \end{cases} B ( i , j ) = ⎩ ⎨ ⎧ ↖ , ← , ↑ , d p ( i , j ) = d p ( i − 1 , j − 1 ) + 1 d p ( i , j ) = d p ( i , j − 1 ) d p ( i , j ) = d p ( i − 1 , j )
伪代码与实现 Algorithm: LCS ( X , Y ) 1. m ← X . l e n g t h 2. n ← Y . l e n g t h 3. f o r i = 1 t o m d o d p [ i , 0 ] ← 0 4. f o r j = 1 t o n d o d p [ 0 , j ] ← 0 5. f o r i = 1 t o m d o 6. f o r j = 1 t o n d o 7. i f x i = y j t h e n 8. d p [ i , j ] ← d p [ i − 1 , j − 1 ] + 1 9. B [ i , j ] ← 【 ↖ 】 10. e l s e i f d p ( i − 1 , j ) ≥ d p ( i , j − 1 ) t h e n 11. d p [ i , j ] ← d p [ i − 1 , j ] 12. B [ i , j ] ← 【 ↑ 】 13. e l s e 14. d p [ i , j ] ← d p [ i , j − 1 ] 15. B [ i , j ] ← 【 ← 】 16. r e t u r n d p [ m , n ] , B \begin{aligned} &\text{Algorithm: }\;\text{LCS}(X,Y)\\\\ 1.&\;m\leftarrow X.length\\ 2.&\;n\leftarrow Y.length\\ 3.&\;\mathbf{for}\;i\;=1\;\mathbf{to}\;m\;\mathbf{do}\;dp[i,0]\leftarrow0\\ 4.&\;\mathbf{for}\;j\;=1\;\mathbf{to}\;n\;\mathbf{do}\;dp[0,j]\leftarrow0\\ 5.&\;\mathbf{for}\;i\;=1\;\mathbf{to}\;m\;\mathbf{do}\\ 6.&\;\qquad\mathbf{for}\;j\;=1\;\mathbf{to}\;n\;\mathbf{do}\\ 7.&\;\qquad\qquad\mathbf{if}\;x_i=y_j\;\mathbf{then}\\ 8.&\;\qquad\qquad\qquad dp[i,j]\leftarrow dp[i-1,j-1]+1\\ 9.&\;\qquad\qquad\qquad B[i,j]\leftarrow 【↖】\\ 10.&\;\qquad\qquad\mathbf{else\;if}\;dp(i-1,j)\geq dp(i,j-1)\;\mathbf{then}\\ 11.&\;\qquad\qquad\qquad dp[i,j]\leftarrow dp[i-1,j]\\ 12.&\;\qquad\qquad\qquad B[i,j]\leftarrow 【↑】\\ 13.&\;\qquad\qquad\mathbf{else}\\ 14.&\;\qquad\qquad\qquad dp[i,j]\leftarrow dp[i,j-1]\\ 15.&\;\qquad\qquad\qquad B[i,j]\leftarrow 【←】\\ 16.&\;\mathbf{return}\;dp[m,n],B \end{aligned} 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. Algorithm: LCS ( X , Y ) m ← X . l e n g t h n ← Y . l e n g t h for i = 1 to m do d p [ i , 0 ] ← 0 for j = 1 to n do d p [ 0 , j ] ← 0 for i = 1 to m do for j = 1 to n do if x i = y j then d p [ i , j ] ← d p [ i − 1 , j − 1 ] + 1 B [ i , j ] ← 【 ↖ 】 else if d p ( i − 1 , j ) ≥ d p ( i , j − 1 ) then d p [ i , j ] ← d p [ i − 1 , j ] B [ i , j ] ← 【 ↑ 】 else d p [ i , j ] ← d p [ i , j − 1 ] B [ i , j ] ← 【 ← 】 return d p [ m , n ] , B
解的追溯将在下面的 c++
实现中给出具体的递归调用实现。
分析伪代码可知,该算法的时间复杂度为O ( m n ) O(mn) O ( mn ) ,空间复杂度为O ( m n ) O(mn) O ( mn ) .
以输入X = < A , B , C , B , D , A , B > , Y = < B , D , C , A , B , A > X =<A, B, C, B, D, A, B> , Y = < B, D, C,A, B,A> X =< A , B , C , B , D , A , B > , Y =< B , D , C , A , B , A > 为例进行展示。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #define M 128 int dp[M][M];int mark[M][M]; int Len (int p) { int res = 0 ; while (p){ res++; p = p/2 ; } return res; } int LCS (char X[], char Y[]) { int m = strlen (X); int n = strlen (Y); memset (dp,0 ,sizeof (dp)); memset (mark,0 ,sizeof (mark)); for (int i = 1 ; i <= m; i++) dp[i][0 ] = 0 ; for (int j = 1 ; j <= n; j++) dp[0 ][j] = 0 ; for (int i = 1 ; i <= m; i++){ for (int j = 1 ; j <= n; j++){ if (X[i-1 ] == Y[j-1 ]){ dp[i][j] = dp[i-1 ][j-1 ]+1 ; mark[i][j] = 2 ; }else if (dp[i-1 ][j] >= dp[i][j-1 ]){ dp[i][j] = dp[i-1 ][j]; mark[i][j] = 1 ; }else { dp[i][j] = dp[i][j-1 ]; mark[i][j] = -1 ; } } } return dp[m][n]; } void Traceback (char X[], int m, int n) { if (mark[m][n] == 2 ){ Traceback (X, m-1 , n-1 ); cout << X[m-1 ] << " " ; }else if (mark[m][n] == -1 ){ Traceback (X, m, n-1 ); }else if (mark[m][n] == 1 ){ Traceback (X, m-1 , n); }else { return ; } } int main (void ) { char A[] = "ABCBDAB" ; char B[] = "BDCABA" ; cout << "min Length = " << LCS (A,B) << endl << endl; Traceback (A, 7 , 6 ); cout << "END" << endl; while (1 ); }
最终的输出结果如下:
投资问题| Investment Problem 投资问题是运筹学 和数学中一种很经典的数学规划模型。 其主要目的是考虑如何将有限的资金投入到若干个项目中,以获得最大的投资回报。具体描述如下:
假设拥有m m m 元钱,可以对n n n 个项目进行投资。将x x x 元投入第i i i 个项目所获得的收益记为f i ( x ) f_i(x) f i ( x ) . 要求给出一种决策,使得所得的收益最大。
可对该问题建立 非线性整数规划模型 :
max ∑ i = 1 n f i ( x i ) s . t . { ∑ i = 1 n x i ≤ m x i ∈ N , i = 1 , 2 , . . , n \begin{aligned} \max &\sum_{i=1}^nf_i(x_i)\\ s.t.&\begin{cases} \sum\limits_{i=1}^nx_i\leq m\\ x_i\in N,i=1,2,..,n \end{cases} \end{aligned} max s . t . i = 1 ∑ n f i ( x i ) ⎩ ⎨ ⎧ i = 1 ∑ n x i ≤ m x i ∈ N , i = 1 , 2 , .. , n
式中,x i x_i x i 表示对第i i i 个项目投入的金额.
状态转移方程 思考本问题的输入,要求对给定金额m m m 和前n n n 个可选项目的投资问题给出最优解。 显然,m , n m,n m , n 就是当前的状态量。最优解依靠当前状态给出,记为d p ( m , n ) dp(m,n) d p ( m , n ) .
从前向后 思考.
当金额为 1 ,可选项目为 1 时,显然将所有金额投入给该项目可获得最大收益,即d p ( 1 , 1 ) = f 1 ( 1 ) dp(1,1)=f_1(1) d p ( 1 , 1 ) = f 1 ( 1 ) . 类似地,当金额为x x x ,可选项目只有 1 时,d p ( x , 1 ) = f 1 ( x ) dp(x,1)=f_1(x) d p ( x , 1 ) = f 1 ( x ) . 当金额为 2,可选项目为 2 时,可能的选择有:将 1 元投给项目 1,将 1 元投给项目 2; 将全部 2 元都投给项目 1; 将全部 2 元都投给项目 2. 从以上可能中取最大 则为最优解。 即d p ( 2 , 2 ) = max { d p ( 1 , 1 ) + f 2 ( 1 ) , d p ( 2 , 1 ) + f 2 ( 0 ) , d p ( 0 , 1 ) + f 2 ( 2 ) } dp(2,2)=\max\{dp(1,1)+f_2(1),dp(2,1)+f_2(0),dp(0,1)+f_2(2)\} d p ( 2 , 2 ) = max { d p ( 1 , 1 ) + f 2 ( 1 ) , d p ( 2 , 1 ) + f 2 ( 0 ) , d p ( 0 , 1 ) + f 2 ( 2 )} 依次类推,则可得到完整的状态转移方程 :
d p ( m , n ) = { max 0 ≤ x ≤ m { d p ( x , n − 1 ) + f n ( m − x ) } , n > 1 f 1 ( m ) , n = 1 dp(m,n)=\begin{cases} \max\limits_{0\leq x\leq m}\{dp(x,n-1)+f_n(m-x)\},&n\gt1\\\\ f_1(m),&n=1 \end{cases} d p ( m , n ) = ⎩ ⎨ ⎧ 0 ≤ x ≤ m max { d p ( x , n − 1 ) + f n ( m − x )} , f 1 ( m ) , n > 1 n = 1
根据动态规划的备忘录 性质,均利用二维数组对结果进行存储,进而得到伪码.
输入:m , n , f [ 1.. n , 1.. m ] m,n,f[1..n,1..m] m , n , f [ 1.. n , 1.. m ] 分别表示持有金额,项目数,第i i i 个项目投入j j j 元的收益 输出:最大收益 Algorithm: Investment ( m , n , f ) 1. r e p e a t d p [ j , i ] ← 0 u n t i l dp初始化完毕 2. f o r i = 1 t o n d o 3. f o r j = 0 t o m d o 4. f o r k = 0 t o j d o 5. d p [ j , i ] ← max ( d p [ j , i ] , d p [ k , i − 1 ] + f [ i , j − k ] ) 6. r e t u r n d p [ m , n ] \begin{aligned} &\text{Algorithm: }\;\text{Investment}(m,n,f)\\\\ 1.&\;\mathbf{repeat}\;dp[j,i]\leftarrow 0\;\mathbf{until}\;\text{dp初始化完毕}\\ 2.&\;\mathbf{for}\;i\;=1\;\mathbf{to}\;n\;\mathbf{do}\\ 3.&\;\qquad\mathbf{for}\;j\;=0\;\mathbf{to}\;m\;\mathbf{do}\\ 4.&\;\qquad\qquad\mathbf{for}\;k\;=0\;\mathbf{to}\;j\;\mathbf{do}\\ 5.&\;\qquad\qquad\qquad dp[j,i]\leftarrow \text{max}(dp[j,i],dp[k,i-1]+f[i,j-k])\\ 6.&\;\mathbf{return}\;dp[m,n] \end{aligned} 1. 2. 3. 4. 5. 6. Algorithm: Investment ( m , n , f ) repeat d p [ j , i ] ← 0 until dp 初始化完毕 for i = 1 to n do for j = 0 to m do for k = 0 to j do d p [ j , i ] ← max ( d p [ j , i ] , d p [ k , i − 1 ] + f [ i , j − k ]) return d p [ m , n ]
显然,对于最内层循环,需要执行j + 1 j+1 j + 1 次加法,j j j 次比较。次内层指示出j : 0 → m j:0\to m j : 0 → m ,最外层指示出执行n n n 次。 从而该算法的时间复杂度T ( m , n ) = ∑ i = 1 n ∑ j = 0 m [ ( j + 1 ) + j ] = O ( n m 2 ) \begin{aligned}T(m,n)=\sum_{i=1}^n\sum_{j=0}^m[(j+1)+j]=O(nm^2)\end{aligned} T ( m , n ) = i = 1 ∑ n j = 0 ∑ m [( j + 1 ) + j ] = O ( n m 2 ) 从备忘录可知,空间复杂度为O ( n m ) O(nm) O ( nm ) .
解的追溯 除了计算最大收益之外,还可以在算法中建立一个标记矩阵从而得以追溯使得最大收益成立的具体决策。
设置标记m a r k [ i , j ] mark[i,j] ma r k [ i , j ] 用于表示对前j j j 个项目投入i i i 元的最优决策时,对前j − 1 j-1 j − 1 个项目投入的金额。 因此得到递推关系:
x n = m − m a r k [ m , n ] x n − 1 = ( m − x n ) − m a r k [ x n , n − 1 ] x n − 2 = ( m − x n − x n − 1 ) − m a r k [ x n − 1 , n − 2 ] ⋯ = ⋯ x 1 = ( m − ∑ k = 2 n x k ) − m a r k [ x 2 , 1 ] \begin{aligned} x_n&=m-mark[m,n]\\ x_{n-1}&=(m-x_n)-mark[x_n,n-1]\\ x_{n-2}&=(m-x_n-x_{n-1})-mark[x_{n-1},n-2]\\ \cdots&=\cdots\\ x_1&=(m-\sum_{k=2}^nx_k)-mark[x_2,1] \end{aligned} x n x n − 1 x n − 2 ⋯ x 1 = m − ma r k [ m , n ] = ( m − x n ) − ma r k [ x n , n − 1 ] = ( m − x n − x n − 1 ) − ma r k [ x n − 1 , n − 2 ] = ⋯ = ( m − k = 2 ∑ n x k ) − ma r k [ x 2 , 1 ]
实例展示与代码 假设给定如下数据,其中手持金额m = 5 m=5 m = 5 ,可选投资项目n = 4 n=4 n = 4 ,对项目i i i 投入x x x 获得的收益f i ( x ) f_i(x) f i ( x ) 如下表所示。
x x x f 1 [ x ] f_1[x] f 1 [ x ] f 2 [ x ] f_2[x] f 2 [ x ] f 3 [ x ] f_3[x] f 3 [ x ] f 4 [ x ] f_4[x] f 4 [ x ] 0 0 0 0 0 1 11 0 2 20 2 12 5 10 21 3 13 10 30 22 4 14 15 32 23 5 15 20 40 24
下面先给出对该问题的 C++
编程实现 :
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 32 33 34 35 36 37 38 39 40 41 #define M 20 int f[5 ][6 ] = {{0 ,0 ,0 ,0 ,0 ,0 }, {0 ,11 ,12 ,13 ,14 ,15 }, {0 ,0 ,5 ,10 ,15 ,20 }, {0 ,2 ,10 ,30 ,32 ,40 }, {0 ,20 ,21 ,22 ,23 ,24 }}; int mark[M][M];int Inverstment (int m, int n) { int dp[M][M]; memset (dp,0 ,sizeof (dp)); memset (mark,0 ,sizeof (mark)); for (int i = 1 ; i <= n; i++){ for (int j = 0 ; j <= m; j++){ for (int k = 0 ; k <= j; k++){ if (dp[k][i-1 ]+f[i][j-k] > dp[j][i]){ dp[j][i] = dp[k][i-1 ]+f[i][j-k]; mark[j][i] = k; } } } } return dp[m][n]; } void get_solve (int m, int n) { vector<int > X = vector <int >(n); for (int i = n-1 ; i >= 0 ; i--){ X[i] = m-mark[m][i+1 ]; m = mark[m][i+1 ]; } for (int i = 0 ; i < X.size (); i++){ cout << "x" << i+1 << "=" << X[i] << endl; } }
根据算法构建备忘录表格:
x x x d p [ x ] [ 1 ] , m a r k [ x ] [ 1 ] dp[x][1],mark[x][1] d p [ x ] [ 1 ] , ma r k [ x ] [ 1 ] d p [ x ] [ 2 ] , m a r k [ x ] [ 2 ] dp[x][2],mark[x][2] d p [ x ] [ 2 ] , ma r k [ x ] [ 2 ] d p [ x ] [ 3 ] , m a r k [ x ] [ 3 ] dp[x][3],mark[x][3] d p [ x ] [ 3 ] , ma r k [ x ] [ 3 ] d p [ x ] [ 4 ] , m a r k [ x ] [ 4 ] dp[x][4],mark[x][4] d p [ x ] [ 4 ] , ma r k [ x ] [ 4 ] 0 0, 0 0, 0 0, 0 0, 0 1 11, 0 11, 1 11, 1 20, 0 2 12, 0 12, 2 13, 1 31, 1 3 13, 0 16, 1 30, 0 33, 2 4 14, 0 21, 1 41, 1 50, 3 5 15, 0 26, 1 43, 1 61, 4
从而,d p [ 5 ] [ 4 ] = 61 dp[5][4]=61 d p [ 5 ] [ 4 ] = 61 即为所求的最大收益 。
根据标记的回溯原则,可以分别得到:( x 1 , x 2 , x 3 , x 4 ) = ( 1 , 0 , 3 , 1 ) (x_1,x_2,x_3,x_4)=(1,0,3,1) ( x 1 , x 2 , x 3 , x 4 ) = ( 1 , 0 , 3 , 1 )
代码的运行结果也正是如此:
1 2 3 4 5 6 7 8 max:61 x1=1 x2=0 x3=3 x4=1
变位压缩| Variable Identification Compression 本问题取自 中国大学MOOC 由北京大学的屈婉玲 教授讲解的 动态规划 一节里的经典例题。
能力有限,我只找到这种特殊的图像变位压缩方法最早出现在 2001年版《计算机算法设计与分析》(王晓东 箸)一书中, 该书涉及的算法示例颇多,并且MOOC上的例题基本出自此书。后来才得知,屈婉玲老师于2011年出版的《算法设计与分析》正是在该书的基础上改进了很多饱受诟病的语言描述和字符混乱问题,并且结合《算法导论》增加了问题的正确性证明,加深了数学语言的表示。
不过对于本节的数字图像变位压缩问题,还是溯源无果,因此此处的英文名取自同样引用王晓东一书的期刊文献:
[1]丁爱芬, 周华君, 石宜金. 一种基于变位与反转变换的动态规划压缩算法[J]. 西南师范大学学报:自然科学版, 2020, 45(1):5.
技术描述 给定一幅灰度图,其像素个数为n n n ,由这n n n 个像素的灰度值构成一个序列{ p 1 , p 2 , . . . , p n } \{p_1,p_2,...,p_n\} { p 1 , p 2 , ... , p n } . 其中,∀ p i , p i ∈ [ 0 : 1 : 255 ] \forall p_i,\;p_i\in[0:1:255] ∀ p i , p i ∈ [ 0 : 1 : 255 ] 表明该图像的灰度值可由8位二进制数组成. 进而该灰度图的像素序列可由8 n b i t 8n\;bit 8 n bi t 的数据流(我们同样用序列{ p 1 , p 2 , . . . , p n } \{p_1,p_2,...,p_n\} { p 1 , p 2 , ... , p n } 表示)构成。
下给出一种变位的压缩思想对该序列实现无损压缩。其主要思想是:
将序列分为m m m 段,分别用S 1 , S 2 , . . . , S m S_1,S_2,...,S_m S 1 , S 2 , ... , S m 表示,使得⋃ i = 1 m S i = { p 1 , p 2 , . . . , p n } \bigcup\limits_{i=1}^m S_i=\{p_1,p_2,...,p_n\} i = 1 ⋃ m S i = { p 1 , p 2 , ... , p n } ; 段S i S_i S i 中的第k k k 个像素点用p k ( i ) p_k^{(i)} p k ( i ) 表示,记S i S_i S i 中像素点的总个数为l i l_i l i ; 对∀ p k ( i ) ∈ S i \forall p_k^{(i)}\in S_i ∀ p k ( i ) ∈ S i 的灰度值不再使用 8 位表示,而采用h i ≤ 8 h_i\leq 8 h i ≤ 8 位进行表示. 则h i h_i h i 的使用实现了像素灰度值位数的变换,也就实现了图像的压缩。下面是h i h_i h i 的具体计算:
选出S i S_i S i 内的最大像素值p k ( i ) p_k^{(i)} p k ( i ) ,其所占的位数,即为h i h_i h i . 有:
h i = ⌈ log 2 ( max p k ( i ) ∈ S i p k ( i ) + 1 ) ⌉ ≤ 8 h_i=\left \lceil\log_2\left(\max\limits_{p_k^{(i)} \in S_i}{p_k^{(i)}}+1\right)\right\rceil\leq 8 h i = ⌈ log 2 ( p k ( i ) ∈ S i max p k ( i ) + 1 ) ⌉ ≤ 8
那么 对于S i S_i S i 内的所有像素点来说,它们的灰度值都小于或等于该像素的灰度值,对应的二进制位数也自然不会高于h i h_i h i . 从而可以对每一段实现数据压缩,进而对整体序列实现压缩。
为了让计算机知道每一个分段下具体有几个像素,灰度值用几位表示,本算法规定对每一段S i S_i S i 的 段头 都先用 11 位的标志位 来记录本段的存储情况. 标志位包括像素个数l i l_i l i (最高256,用 9 位表示)和灰度值位数h i h_i h i (最高 8 位,用 3 位表示)
也就是说,任意一段S i S_i S i 的长度应该是:11 + h i × l i 11+h_i\times l_i 11 + h i × l i .
从而,最优 的压缩就是寻找到这样一种分段策略T = { S 1 , S 2 , ⋯ , S m } T=\{S_1,S_2,\cdots,S_m\} T = { S 1 , S 2 , ⋯ , S m } 使得总的存储位数最小 。即:
max T ∑ i = 1 m ( 11 + h i l i ) \max_{T}\sum_{i=1}^m(11+h_il_i) T max i = 1 ∑ m ( 11 + h i l i )
状态转移方程 考虑从p 1 p_1 p 1 开始到某个像素点p i p_i p i 这一子序列的最小存储位数划分问题。我们将末尾像素的下标i i i 作为状态,其最小存储位数就记为d p ( i ) dp(i) d p ( i ) .
从后往前 思考,假设已知d p ( 1 ) ⋯ d p ( i − 1 ) dp(1)\cdots dp(i-1) d p ( 1 ) ⋯ d p ( i − 1 ) 的值和分割结果,那么该问题可能的划分有:
末尾的单像素{ p i } \{p_{i}\} { p i } 自成一个划分S m S_m S m 再与通过d p ( i − 1 ) dp(i-1) d p ( i − 1 ) 划分好的序列T ′ = { S 1 , S 2 , . . . , S m − 1 } T'=\{S_1,S_2,...,S_{m-1}\} T ′ = { S 1 , S 2 , ... , S m − 1 } 共同构成一个可能的划分T = { S 1 , S 2 , ⋯ , S m } T=\{S_1,S_2,\cdots,S_m\} T = { S 1 , S 2 , ⋯ , S m } . 末尾的两个像素{ p i − 1 , p i } \{p_{i-1},p_i\} { p i − 1 , p i } 自成一个划分S m S_m S m 再与通过d p ( i − 2 ) dp(i-2) d p ( i − 2 ) 划分好的序列T ′ = { S 1 , S 2 , . . . , S m − 1 } T'=\{S_1,S_2,...,S_{m-1}\} T ′ = { S 1 , S 2 , ... , S m − 1 } 共同构成一个可能的划分T = { S 1 , S 2 , ⋯ , S m } T=\{S_1,S_2,\cdots,S_m\} T = { S 1 , S 2 , ⋯ , S m } . 末尾的3个像素{ p i − 2 , p i − 1 , p i } \{p_{i-2},p_{i-1},p_i\} { p i − 2 , p i − 1 , p i } 自成一个划分S m S_m S m 再与通过d p ( i − 2 ) dp(i-2) d p ( i − 2 ) 划分好的序列T ′ = { S 1 , S 2 , . . . , S m − 1 } T'=\{S_1,S_2,...,S_{m-1}\} T ′ = { S 1 , S 2 , ... , S m − 1 } 共同构成一个可能的划分T = { S 1 , S 2 , ⋯ , S m } T=\{S_1,S_2,\cdots,S_m\} T = { S 1 , S 2 , ⋯ , S m } . ...
将整个序列{ p 1 , p 2 , . . . , p i } \{p_1,p_2,...,p_i\} { p 1 , p 2 , ... , p i } 直接看成一个划分. (如果规定d p ( 0 ) = 0 dp(0)=0 d p ( 0 ) = 0 ,则可以和以上情况统一起来) 11 tag S S m h[i-j+1, i]×j dp[i-j] S m 1 ... 如图所示,对于上述的第j j j 种情况而言,可以直接计算出在此划分下的存储位数:
d p ( i − j ) + h ( i − j + 1 , i ) × j + 11 dp(i-j)+h(i-j+1,i)\times j+11 d p ( i − j ) + h ( i − j + 1 , i ) × j + 11
其中,h ( i − j + 1 , i ) h(i-j+1,i) h ( i − j + 1 , i ) 表示的是从后往前取j j j 个像素自成一组时(如果是S m S_m S m 的话),其最高灰度值所占的位数h m h_m h m . 有:
h m = ⌈ log 2 ( max p k ( m ) ∈ S m p k ( m ) + 1 ) ⌉ = ⌈ log 2 ( max i − j + 1 ≤ k ≤ i p k + 1 ) ⌉ = h ( i − j + 1 , i ) \begin{aligned} h_m&=\left \lceil\log_2\left(\max\limits_{p_k^{(m)} \in S_m}{p_k^{(m)}}+1\right)\right\rceil\\ &=\left \lceil\log_2\left(\max\limits_{i-j+1\leq k\leq i}{p_k}+1\right)\right\rceil=h(i-j+1,i) \end{aligned} h m = ⌈ log 2 ( p k ( m ) ∈ S m max p k ( m ) + 1 ) ⌉ = ⌈ log 2 ( i − j + 1 ≤ k ≤ i max p k + 1 ) ⌉ = h ( i − j + 1 , i )
所以,我们只需搜索所有可能的分割点j : 1 → min { i , 256 } j:1\to \min\{i,256\} j : 1 → min { i , 256 } ,从中求出最小的存储位数即d p ( i ) dp(i) d p ( i ) 。最高取 256 是因为一个分割的最大像素个数已经被规定为 256.
最终得到状态转移方程如下:
d p ( i ) = { min 1 ≤ j ≤ i { d p ( i − j ) + h ( i − j + 1 , i ) × j + 11 } , n ≥ 1 0 , n = 0 dp(i)=\begin{cases} \min\limits_{1\leq j\leq i}\{dp(i-j)+h(i-j+1,i)\times j+11\},&n\geq1\\ 0,&n=0 \end{cases} d p ( i ) = { 1 ≤ j ≤ i min { d p ( i − j ) + h ( i − j + 1 , i ) × j + 11 } , 0 , n ≥ 1 n = 0
伪码与性能分析 分析转移方程可知,d p ( n ) dp(n) d p ( n ) 就是我们需要求解的结果。而其依赖于d p ( 1.. n − 1 ) dp(1..n-1) d p ( 1.. n − 1 ) . 并且,需要自底向上,从小到大地求解并存储子问题的答案。 因此,我们利用一维数组d p [ 0.. n ] dp[0..n] d p [ 0.. n ] 存储动态规划表,构建循环体i : 1 → n i:1\to n i : 1 → n 设计算法。
为了追溯划分策略 ,还可以建立一维数组s p l i t [ 1.. n ] split[1..n] s pl i t [ 1.. n ] 记录状态为i i i 时,划分出来的末尾序列的起点下标。
为了避免繁琐的计算,算法对输入的数组p [ 1.. n ] p[1..n] p [ 1.. n ] 生成对应下标的灰度值位数数组h [ 1.. n ] h[1..n] h [ 1.. n ] ,使得h [ i ] h[i] h [ i ] 存储的是对应下标的像素p i p_i p i 的灰度值的位数,即h [ i ] = ⌈ log 2 ( p [ i ] + 1 ) ⌉ h[i]=\left \lceil\log_2\left(p[i]+1\right)\right\rceil h [ i ] = ⌈ log 2 ( p [ i ] + 1 ) ⌉ .
所以,对于转移方程中的h ( i − j + 1 , i ) h(i-j+1,i) h ( i − j + 1 , i ) 也不需要重复计算,只需用一个变量h m a x hmax hma x 在每次迭代循环时判断是否存在新的最大位数即可。
算法伪码如下:
Algorithm: Compression ( p , n ) 1. 初始化数组 d p , s p l i t 2. d p [ 0 ] ← 0 3. f o r i = 1 t o n d o 4. h m a x ← h [ i ] ← ⌈ log 2 ( p [ i ] + 1 ) ⌉ 5. d p [ i ] ← d p [ i − 1 ] + h m a x 6. s p l i t [ i ] ← 1 7. f o r j = 2 t o min { i , 256 } d o 8. h m a x ← max ( h m a x , h [ i − j + 1 ] ) 9. t m p ← d p [ i − j ] + j × h m a x 10. i f t m p < d p [ i ] t h e n 11. d p [ i ] ← t m p 12. s p l i t [ i ] ← j 13. d p [ i ] ← d p [ i ] + 11 14. r e t u r n d p , s p l i t \begin{aligned} &\text{Algorithm: }\;\text{Compression}(p,n)\\\\ 1.&\;\text{初始化数组 }dp,\;split\\ 2.&\;dp[0]\leftarrow0\\ 3.&\;\mathbf{for}\;i\;=1\;\mathbf{to}\;n\;\mathbf{do}\\ 4.&\;\qquad hmax\leftarrow h[i]\leftarrow\left \lceil\log_2\left(p[i]+1\right)\right\rceil\\ 5.&\;\qquad dp[i]\leftarrow dp[i-1]+hmax\\ 6.&\;\qquad split[i]\leftarrow 1\\ 7.&\;\qquad\mathbf{for}\;j\;=2\;\mathbf{to}\;\min\{i,256\}\;\mathbf{do}\\ 8.&\;\qquad\qquad hmax\leftarrow \max(hmax,h[i-j+1])\\ 9.&\;\qquad\qquad tmp\leftarrow dp[i-j]+j\times hmax\\ 10.&\;\qquad\qquad \mathbf{if}\;tmp\lt dp[i]\;\mathbf{then}\\ 11.&\;\qquad\qquad\qquad dp[i]\leftarrow tmp\\ 12.&\;\qquad\qquad\qquad split[i]\leftarrow j\\ 13.&\;\qquad\qquad dp[i]\leftarrow dp[i]+11\\ 14.&\;\mathbf{return}\;dp,\;split \end{aligned} 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. Algorithm: Compression ( p , n ) 初始化数组 d p , s pl i t d p [ 0 ] ← 0 for i = 1 to n do hma x ← h [ i ] ← ⌈ log 2 ( p [ i ] + 1 ) ⌉ d p [ i ] ← d p [ i − 1 ] + hma x s pl i t [ i ] ← 1 for j = 2 to min { i , 256 } do hma x ← max ( hma x , h [ i − j + 1 ]) t m p ← d p [ i − j ] + j × hma x if t m p < d p [ i ] then d p [ i ] ← t m p s pl i t [ i ] ← j d p [ i ] ← d p [ i ] + 11 return d p , s pl i t
【注】一些说明:
伪码中的j j j 是从 2
开始的。这是因为在 4 至 6 行 我们直接对j = 1 j=1 j = 1 的情况进行了处理 因为转移方程中存在常数 11
,它不影响比较,因此统一将 11
放到一次大循环后添加(第 13 行) 第 4 行的h [ i ] h[i] h [ i ] 总是随着i i i 的增大不断填充,并且此时h m a x hmax hma x 总是状态为i i i 时最右端的位数。而随着j j j 的增加,末尾序列从当前最右端p i p_i p i 逐步往左延长,所以在 第 8 行h m a x hmax hma x 只需与当前划分的最左端p i − j + 1 p_{i-j+1} p i − j + 1 的位数进行对比即可,而不需要再从最右端开始遍历。 由于j j j 的循环最高256 256 256 次,与n n n 无关,因此即使n n n 很大j j j 的循环次数最坏也是W ( n ) = 256 = O ( 1 ) W(n)=256=O(1) W ( n ) = 256 = O ( 1 ) 。而i i i 的循环依赖于n n n ,所以本算法总体的时间复杂度为O ( n ) O(n) O ( n ) .
根据备忘录的大小可知,本算法的空间复杂度应为O ( n ) O(n) O ( n ) .
编程实现 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #define M 256+1 int dp[M];int split[M];int Len (int p) { int res = 0 ; while (p){ res++; p = p/2 ; } return res; } int Compression (int p[], int n) { memset (dp,0 ,sizeof (dp)); memset (split,0 ,sizeof (split)); int h[M] = {0 }; int hmax = 0 ; dp[0 ] = 0 ; for (int i = 1 ; i <= n; i++){ h[i] = Len (p[i]); hmax = h[i]; dp[i] = dp[i-1 ]+hmax; split[i] = i; for (int j = 2 ; j <= min (i,256 ); j++){ hmax = max (hmax,h[i-j+1 ]); int tmp = dp[i-j]+j*hmax; if (dp[i] > tmp){ dp[i] = tmp; split[i] = j; } } dp[i] += 11 ; } return dp[n]; } void Traceback (int n) { int i = 1 ; int right = n; int left = right-split[right]+1 ; while (right){ cout << "group " << i << ": (" << left << ", " << right << ")" << endl; right = left-1 ; left = right-split[right]+1 ; i++; } }
输出结果如下:
1 2 3 4 min bit = 57 group 1 : (5 , 6 ) group 2 : (1 , 4 )
表示将 [1-4] 分为一组,[5-6]分为一组时,可使得存储位数最小,且最小为57位。
数塔问题 HDU
贪心 暴力 O(2^n) DP O(n^2/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 32 #include <iostream> #include <string.h> using namespace std;int maxof3 (int a,int b,int c) { int tmp = max (a,b); return max (tmp,c); } int dp[100001 ][12 ];int main (void ) { int n; int x,y,x_max=0 ; int i,j; while (cin >> n){ if (n == 0 ) return -1 ; memset (dp,0 ,sizeof (dp)); for (i = 0 ; i < n; i++){ scanf ("%d%d" ,&y,&x); dp[x][y]++; x_max = max (x_max,x); } for (i=x_max;i>0 ;i--) { for (j=0 ;j<=10 ;j++) dp[i-1 ][j]+=maxof3 (dp[i][j],dp[i][j+1 ],dp[i][j-1 ]); } cout << dp[0 ][5 ] << endl; } return 0 ; }
搬寝室
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 32 #include <iostream> #include <string.h> #include <stdio.h> #include <algorithm> #define inf 0x3f3f3f3f using namespace std;int dp[2000 ][1000 ];int main (void ) { int n,k; int i,j; int a[2000 ]; while (~scanf ("%d%d" ,&n,&k)){ memset (a,0 ,sizeof (a)); memset (dp,inf,sizeof (dp)); for (i = 0 ; i < n; i++){ scanf ("%d" ,&a[i]); } sort (a,a+n); dp[0 ][0 ]=dp[1 ][0 ]=0 ; dp[2 ][1 ] = (a[0 ]-a[1 ])*(a[0 ]-a[1 ]); for (i = 2 ; i <= n; i++){ dp[i][0 ]=0 ; for (j = 1 ; j <= k; j++){ dp[i][j] = min (dp[i-1 ][j],dp[i-2 ][j-1 ]+(a[i-1 ]-a[i-2 ])*(a[i-1 ]-a[i-2 ])); } } cout << dp[n][k] << endl; } return 0 ; }
最长上升子序列 小规模
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <bits/stdc++.h> using namespace std;typedef long long LL;const LL maxn = 1000010 ;const LL mod = 1000000007 ;LL a[maxn]; int dp[maxn];#define CPC int main (void ) {#ifdef CPC char bufin[40012 ],bufout[40012 ]; for (int t = 1 ; t <= 1 ; t++){ sprintf (bufin,"%02d.in" , t); sprintf (bufout,"%02d.out2" ,t); freopen (bufin,"rb" , stdin); freopen (bufout, "wb" , stdout); #endif LL n, b; while (~scanf ("%lld%lld" ,&n,&b)) { a[1 ] = b; dp[1 ] = 1 ; for (int i = 2 ; i <= n; i++){ a[i] = 1LL *(a[i-1 ]+1 )*(a[i-1 ]+1 )%mod; dp[i] = 1 ; } int res = 0 ; for (int i = n-1 ; i >= 1 ; i--){ for (int j = i+1 ; j <= n; j++){ if (a[j] > a[i]){ dp[i] = max (dp[i], dp[j]+1 ); } } res = max (dp[i], res); } printf ("%d\n" ,res); } #ifdef CPC } #endif return 0 ; }
大规模
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <bits/stdc++.h> using namespace std;typedef long long LL;const LL maxn = 1000010 ;const LL mod = 1000000007 ;LL a[maxn]; int dp[maxn];#define CPC int main (void ) {#ifdef CPC char bufin[40012 ],bufout[40012 ]; for (int t = 1 ; t <= 1 ; t++){ sprintf (bufin,"%02d.in" , t); sprintf (bufout,"%02d.out2" ,t); freopen (bufin,"rb" , stdin); freopen (bufout, "wb" , stdout); #endif LL n, b; while (~scanf ("%lld%lld" ,&n,&b)) { a[1 ] = b; dp[1 ] = mod; for (int i = 2 ; i <= n; i++){ a[i] = 1LL *(a[i-1 ]+1 )*(a[i-1 ]+1 )%mod; dp[i] = mod; } int k = 1 ; for (int i = 1 ; i <= n; i++){ if (dp[k] < a[i]) dp[++k] = a[i]; else { *lower_bound (dp+1 , dp+k, a[i]) = a[i]; } } printf ("%d\n" ,k); } #ifdef CPC } #endif return 0 ; }
待更参考:https://zhuanlan.zhihu.com/p/121032448
参考 《算法导论 (原书第三版)》 算法设计与分析|中国大学MOOC HDForum-动态规划|by杭电刘春英 图像压缩问题|博客园 谈谈.动态规划|DynamicProgramming