填充dp数组的方式 目前碰到的题目中,一般填充dp要么是按照顺序一个一个填,要么是先全初始化为-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 class  Solution   {         public  int  lengthOfLIS (int [] nums)   {         if (nums.length==0 ){             return  0 ;         }         int [] dp = new  int [nums.length];         int  max = 0 ;         for (int  i=0 ;i<nums.length;i++){             dp[i] = 1 ;             if (i>0 ){                 for (int  k=0 ;k<i;k++){                     if (nums[i]>nums[k]){                         dp[i] = Math.max(dp[i],dp[k]+1 );                     }                 }             }             max = Math.max(max,dp[i]);         }         return  max;     } } 
 
按需要计算 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 class  Solution   {    private  Integer[] dp;     private  int [] nums;     public  int  lengthOfLIS (int [] nums)   {         if (nums.length==0 ){             return  0 ;         }         this .nums = nums;         dp = new  Integer[nums.length];         Arrays.fill(dp,-1 );         int  max = 0 ;         for (int  i=0 ;i<nums.length;i++){             max = Math.max(max,getDp(i));         }         return  max;     }     public  int  getDp (int  i)  {         if (dp[i]==-1 ){             dp[i] = 1 ;             if (i>0 ){                 for (int  k=0 ;k<i;k++){                     if (nums[i]>nums[k]){                         dp[i] = Math.max(dp[i],getDp(k)+1 );                     }                 }             }         }         return  dp[i];     } } 
 
一些想法 在这个例子中,由于dp的每个元素都需要填充(然后找最大值),而且可以方便的按照顺序遍历,所以方法一显得简单一些
但是有的题目中顺序不是很好找,比如有的二维dp数组中,要填充的是三角形区域,有的是从左往右扫,有的是从上往下扫,其它一些奇怪的dp几乎找不到顺序
而且很多题目中并不需要把整个dp都填满,只需要得到特定一个元素即为答案,其实没必要按顺序把所有的元素都算出来
1 2 3 4 5 6 7 8 9 10 public  int  getDp (int  i)  {  if (dp[i]==-1 ){             }         return  dp[i]; } 
 
所以感觉,很容易找到顺序、或者必须填满的可以用方法一,其它的直接getDp()省脑细胞
一维  例1 最长上升子序列 (LeetCode 300) link 
描述 给定一个无序的整数数组,找到其中最长上升子序列的长度
状态表示 1 dp[i] 表示第i个元素(包含i)之前最大上升子序列的长度 
 
状态转移方程 1 2 dp[i] = max{ dp[k] + 1} 其中 0<=k<i 且 nums[i]>nums[k] 
 
实现 见:填充dp数组的方式 
二维 例1 最长公共子序列问题(算法设计技巧与分析 沙特版 p130) 描述 找两个字符串的最长公共子序列长度
如A=zxyxyz B=xyyzx 则最长公共子序列为xyyz
状态表示 1 2 令 A = a1a2…an,B = b1b2…bn L[i , j] 表示 a1a2…ai 和 b1b2…bj 的最长公共子序列的长度 
 
状态转移方程 1 2 当 A[i] == B[i],L[i , j] = L[i-1 , j-1] + 1 当 A[i] != B[i],L[i , j] = max{ L[i-1 , j] , L[i , j-1] } 
 
eg: A = horse,B = ros i为2,j为2时,o == o,L[ ho , ro ] = L[ h , r ] + 1 i为3,j为3时,r != s,L[ hor , ros ] = max{ L[ ho , ros ] , L[ hor , ro ] }
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public  static  int  LCS (String word1, String word2)   {	   int [][] dp = new  int [word1.length()+1 ][word2.length()+1 ];      	for (int  i=1 ;i<word1.length()+1 ;i++) { 		for (int  j=1 ;j<word2.length()+1 ;j++) { 			if (word1.charAt(i-1 )==word2.charAt(j-1 )) { 				dp[i][j] = dp[i-1 ][j-1 ] + 1 ; 			}else  { 				dp[i][j] = Math.max(dp[i-1 ][j], dp[i][j-1 ]); 			} 		} 	}    	return  dp[word1.length()][word2.length()]; } 
 
例2 编辑距离 (LeetCode 72) link 
描述 给定两个单词 word1 和 word2,计算将 word1 转换成 word2 所使用的最少操作数
操作包括:
状态表示 1 2 令 A = a1a2…an,B = b1b2…bn L[i , j] 表示 a1a2…ai 和 b1b2…bj 的最短编辑距离 
 
和公共子序列一样
状态转移方程 1 2 3 当 A[i] == B[i],L[i , j] = L[i-1 , j-1] 当 A[i] != B[i],L[i , j] = min{ L[i-1 , j-1] , L[i-1 , j] , L[i , j-1] } + 1 其中,L[i-1 , j-1]代表替换操作,L[i-1 , j]代表删除操作,L[i , j-1]代表插入操作 
 
eg: A = horse,B = ros i为2,j为2时,o == o,L[ ho , ro ] = L[ h , r ],无需继续编辑 i为3,j为3时,r != s,L[ hor , ros ] = min{ L[ ho , ro ] , L[ ho , ros ] , L[ hor , ro ] } + 1 L[ ho , ro ] + 1 代表,在ho已经编辑成ro之后,将r替换成s L[ ho , ros ] + 1 代表,在ho已经编辑成ros之后,将r删除 L[ hor , ro ] + 1 代表,在hor已经编辑成ro之后,插入s
实现 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 class  Solution   {    public  static  int  minDistance (String word1, String word2)   { 	         int [][] dp = new  int [word1.length()+1 ][word2.length()+1 ];              for (int  i=1 ;i<word1.length()+1 ;i++) {     	    dp[i][0 ] = i;         }         for (int  i=1 ;i<word2.length()+1 ;i++) {     	    dp[0 ][i] = i;         }          	for (int  i=1 ;i<word1.length()+1 ;i++) { 	    	for (int  j=1 ;j<word2.length()+1 ;j++) { 		    	if (word1.charAt(i-1 )==word2.charAt(j-1 )) { 			    	dp[i][j] = dp[i-1 ][j-1 ];     			}else  { 	    			int  min = Math.min(dp[i-1 ][j], dp[i][j-1 ]); 		    		min = Math.min(min, dp[i-1 ][j-1 ]); 			    	dp[i][j] = min+1 ; 			    }     		} 	    }	 	 	    return  dp[word1.length()][word2.length()];     } } 
 
注意 这题dp第0行和第0列初始化为0,1,2,3…
例3 回文子串 (LeetCode 647) link 
描述 统计给定字符串中回文子串的个数
如”aaa”中有”a”, “a”, “a”, “aa”, “aa”, “aaa”共6个
状态表示 1 dp[i][j] 表示字符串的第i到j位(含)是否为回文串 
 
状态转移方程 见注释
实现 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 class  Solution   {    Integer[][] dp;     public  int  countSubstrings (String s)   {         if (s.length()==0 ){             return  0 ;         }                  dp = new  Integer[s.length()][s.length()];         for (int  i=0 ;i<s.length();i++){             Arrays.fill(dp[i],-1 );         }                  int  cnt = 0 ;         for (int  i=0 ;i<s.length();i++){             for (int  j=i;j<s.length();j++){                 if (getDp(i,j,s)==1 ){                     cnt++;                 }             }         }         return  cnt;     }     public  int  getDp (int  i,int  j,String s)  {         if (dp[i][j]==-1 ){             if (i==j){                                  dp[i][j] = 1 ;             }else  if (s.charAt(i)!=s.charAt(j)){                                  dp[i][j] = 0 ;             }else {                 if (j-i==1 ){                                          dp[i][j] = 1 ;                 }else {                                          if (getDp(i+1 ,j-1 ,s)==1 ){                                                  dp[i][j] = 1 ;                     }else {                         dp[i][j] = 0 ;                     }                 }             }         }         return  dp[i][j];     } } 
 
三维 例1 二指输入的的最小距离 (LeetCode 1320) link 
描述  
二指输入法键盘上,每个字母可用坐标表示,如P(2,3)
给定一个待输入字符串,计算仅用两根手指,输入该字符串的最小移动距离(起始位置任意,代价位0)
距离按 |x1-x2| + |y1-y2| 计算
状态表示 1 2 dp[i][l][r] 表示输入完第i个字母后,左手位置为l,右手位置为r l、r对应字母编号,如A-0,B-1 
 
状态转移方程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 记第i个字母位cur,第i-1个字母为pre 1.左手移动到cur处 	- 移动前左手在pre处,则左手必然从pre移到cur,右手可以在任意位置m 	   dp[i][cur][m] = dp[i-1][pre][m] + dis(pre,cur); 	- 移动前右手在pre处(即m==pre),则左手可以从任意位置n移到cur 	   dp[i][cur][m] = dp[i-1][n][m] + dis(n,cur); 故 dp[i][cur][m] = min{ dp[i-1][pre][m] , dp[i-1][n][pre] } 其中m取值0~26,n取值0~26 2.右手移动到cur处 	- 移动前右手在pre处,则右手必然从pre移到cur,左手可以在任意位置m 	   dp[i][m][cur] = dp[i-1][m][pre] + dis(pre,cur); 	- 移动前左手在pre处(即m==pre),则右手可以从任意位置n移到cur 	   dp[i][m][cur] = dp[i-1][m][n] + dis(n,cur); 同上,dp[i][cur][m] = min{ dp[i-1][pre][m] , dp[i-1][n][pre] } 
 
实现 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 class  Solution   {    public  int  minimumDistance (String word)   {         int [] words = new  int [word.length()];         for (int  i=0 ;i<word.length();i++){             words[i] = word.charAt(i) - 'A' ;         }         int [][][] dp = new  int [words.length][26 ][26 ];         for (int  i=0 ;i<26 ;i++){                          dp[0 ][words[0 ]][i] = 0 ;                          dp[0 ][i][words[0 ]] = 0 ;         }         for (int  i=1 ;i<words.length;i++){             int  cur = words[i];             int  pre = words[i-1 ];             int  dis = calDis(cur,pre);                          for (int  m=0 ;m<26 ;m++){                                  dp[i][cur][m] = dp[i-1 ][pre][m] + dis;                                  dp[i][m][cur] = dp[i-1 ][m][pre] + dis;                 if (m==pre){                                          for (int  n=0 ;n<26 ;n++){                         int  dis2 = calDis(n,cur);                                                  dp[i][cur][m] = Math.min(dp[i][cur][m],dp[i-1 ][n][m]+dis2);                                                  dp[i][m][cur] = Math.min(dp[i][m][cur],dp[i-1 ][m][n]+dis2);                     }                 }             }         }         int  min = Integer.MAX_VALUE;         int  last = words[words.length-1 ];         for (int  m=0 ;m<26 ;m++){             min = Math.min(min,dp[words.length-1 ][last][m]);             min = Math.min(min,dp[words.length-1 ][m][last]);         }         return  min;     }     public  int  calDis (int  a, int  b)  {         int  x1 = a / 6 , y1 = a % 6 ;         int  x2 = b / 6 , y2 = b % 6 ;         return  (int )(Math.abs(x1 - x2)) + (int )(Math.abs(y1 - y2));     } } 
 
优化为二维 由于两只手是完全对称的,不需要知道具体哪只手在cur处,只要有一只在就行了
状态表示 1 dp[i][rest] 表示一只手在cur,另一只手在rest处时的最小移动距离 
 
状态转移方程 1 2 3 4 5 一只手从pre移动到cur,另一只手随便在哪 dp[i][anywhere] = dp[i-1][anywhere] + dis(pre,cur) 若另一只手恰好在pre处,这只手可以从任何地方移动到cur dp[i][pre] = dp[i-1][anywhere] + dis(anywhere,cur) 
 
实现 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 class  Solution   {    public  int  minimumDistance (String word)   {         int [] words = new  int [word.length()];         for (int  i=0 ;i<word.length();i++){             words[i] = word.charAt(i) - 'A' ;         }         int [][] dp = new  int [words.length][26 ];         for (int  i=1 ;i<words.length;i++){             int  cur = words[i];             int  pre = words[i-1 ];             int  dis = calDis(cur,pre);             for (int  m=0 ;m<26 ;m++){                                  dp[i][m] = dp[i-1 ][m] + dis;                 if (m==pre){                                          for (int  n=0 ;n<26 ;n++){                         dp[i][m] = Math.min(dp[i][m],dp[i-1 ][n]+calDis(n,cur));                     }                 }             }         }         int  min = Integer.MAX_VALUE;         for (int  m=0 ;m<26 ;m++){             min = Math.min(min,dp[words.length-1 ][m]);         }         return  min;     }     public  int  calDis (int  a, int  b)  {         int  x1 = a / 6 , y1 = a % 6 ;         int  x2 = b / 6 , y2 = b % 6 ;         return  (int )(Math.abs(x1 - x2)) + (int )(Math.abs(y1 - y2));     } } 
 
优化前 执行用时:19 ms  内存消耗:42.6 MB 
优化后 执行用时:7 ms  内存消耗:38.1 MB 
也不多的样子…但是思路简单多了
状态压缩 将状态用二进制串表示,二进制串以int形式作为下标
例1 参加考试的最大学生数 (LeetCode 1349) link 
描述  
教室里有的座位是坏的 
学生可以看到左、右、左上、右上方向的试卷 
 
计算该考场可以容纳的一起参加考试且无法作弊的最大学生人数
状态压缩 用一个二进制串表示一行中每个位置有没有人坐
如 0011 表示 无无有有,记为3
状态表示 1 dp[row][pre] 表示当row-1行的坐法为pre时,第row行及后面所有行最多坐多少人 
 
其中,pre初始化的大小为 1 << 列数
如 有6列,最多 2 ^ 6 种坐法,即 1 << 6
状态转移方程 1 2 dp[row][pre] = max{ dp[row+1][cur] + Integer.bitCount(cur) } 其中cur是row行所有可行状态 
 
每次需先判断坐法为 cur 是否可行
结果 1 dp[0][0] 即为 第-1行没人坐时,第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 32 33 34 35 36 37 38 class  Solution   {    private  char [][] seats;     private  Integer[][] dp;     public  int  maxStudents (char [][] seats)   {         this .seats = seats;         dp = new  Integer[seats.length][1  << seats[0 ].length];         int  ret = getDp(0 ,0 );         return  ret;     }     public  int  getDp (int  row,int  pre)  {         if (row == seats.length){             return  0 ;         }         if (dp[row][pre] == null ){             int  res = 0 ;                          for (int  i=0 ;i<dp[0 ].length;i++){                                  if (isValid(row,pre,i)){                     int  backNum = getDp(row+1 ,i);                          int  curNum = Integer.bitCount(i);                      res = Math.max(res,backNum+curNum);                 }             }             dp[row][pre] = res;         }         return  dp[row][pre];     }   	     private  boolean  isValid (int  row, int  pre, int  cur)   {  }