动态规划
确定dp数组以及下标的含义
确定递归公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
斐波那契数(与爬楼梯思路一样)
public int fib(int n) {
//dp是第i下标的斐波那契数
int[] dp=new int[n+1];
if(n==0){
return 0;
}
if(n==1){
return 1;
}
dp[0]=0;
dp[1]=1;
for (int i = 2; i < dp.length; i++) {
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
使用最小花费爬楼梯
public int minCostClimbingStairs(int[] cost) {
int[] dp=new int[cost.length+1];
dp[0]=0;
dp[1]=0;
for(int i=2;i<dp.length;i++){
dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[dp.length-1];
}
不同路径
使用二维dp数组,初始化第一行为1,第一列也为1.
public int uniquePaths(int m, int n) {
int[][] dp=new int[m][n];
for(int i=0;i<m;i++){
dp[i][0]=1;
}
for(int j=0;j<n;j++){
dp[0][j]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
路径设置障碍物
初始化中,如果第一行中有障碍物,那么该行中障碍物后面的位置都不可到达
同时第一列中如果有障碍物,那么该列障碍物后面的位置都不可达。
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int[][] dp=new int[obstacleGrid.length][obstacleGrid[0].length];
for(int i=0;i<dp.length;i++){
if(obstacleGrid[i][0]==1){
while(i<dp.length){
dp[i][0]=0;
i++;
}
}else{
dp[i][0]=1;
}
}
for(int j=0;j<dp[0].length;j++){
if(obstacleGrid[0][j]==1){
while(j<dp[0].length){
dp[0][j]=0;
j++;
}
}else{
dp[0][j]=1;
}
}
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(obstacleGrid[i][j]==1){
dp[i][j]=0;
}else{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[dp.length-1][dp[0].length-1];
}
整数拆分
dp公式总共考虑到三个情况,分别是拆分成两个整数的最大值,拆分成三个及以上的整数最大值,还有和它自己的当前值进行比较。
public int integerBreak(int n) {
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=0;
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=2;j<i;j++){
int temp=Math.max(dp[i-j]*j,(i-j)*j);
dp[i]=Math.max(temp,dp[i]);
}
}
return dp[n];
}
不同的二叉搜索树
空树也是一种二叉搜索树,所以初始化dp[0]=1
不同的二叉搜索树个数为左子树个数对应的搜索树数量*右子树对应的搜索树数量,将不同的情况相加
因为这个不看具体的值,只是看子搜索树的形状。
public int numTrees(int n) {
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;i++){
for(int j=0;j<i;j++){
dp[i]=dp[i]+dp[j]*dp[i-j-1];
}
}
return dp[n];
}
0-1背包问题
重点:dp[i][j]表示从第0~i个物品中任选,装入背包重量为j时,最大的价值总和
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]),其中j-weight[i]需要判断是否<0
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
int M=scanner.nextInt();
int N=scanner.nextInt();
int[] weight=new int[M];
for (int i = 0; i < M; i++) {
weight[i]=scanner.nextInt();
}
int[] value=new int[M];
for (int i = 0; i < M; i++) {
value[i]=scanner.nextInt();
}
int[][] dp=new int[M][N+1];
//第一行初始化
for (int j = 0; j < N + 1; j++) {
if (weight[0]<=j)
dp[0][j]=value[0];
}
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
if (j-weight[i]>=0){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}else {
dp[i][j]=dp[i-1][j];
}
}
}
System.out.println(dp[M-1][N]);
}
}
0-1背包使用滚动数组
重点:dp[j]的含义:是背包重量为j时的最大价值总和
注意里层循环的截止条件,不是>0,而是>=weight[i],而且是从后往前遍历,否则会重复添加(先循环物品,再循环重量)
// 创建一个动态规划数组 dp,初始值为 0
int[] dp = new int[N + 1];
// 外层循环遍历每个类型的研究材料
for (int i = 0; i < M; i++) {
// 内层循环从 N 空间逐渐减少到当前研究材料所占空间
for (int j = N; j >= costs[i]; j--) {
// 考虑当前研究材料选择和不选择的情况,选择最大值
dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]);
}
}
// 输出 dp[N],即在给定 N 行李空间可以携带的研究材料的最大价值
System.out.println(dp[N]);
scanner.close();
目标和 (装满背包有几种方法?)
一维数组是在上一行结果的基础上进行下一行的计算,因此遍历数组必须在外面
dp[j]:表示当背包重量是j时,装满的方式有几种
dp[j]=dp[j]+dp[j-nums[i]],不装最后一个物品和装最后一个物品的装满背包总和
public int findTargetSumWays(int[] nums, int target) {
int sum =Arrays.stream(nums).sum();//
int bagSize=(sum+target)/2;//得到target时的最大背包重量
if (sum< Math.abs(target) || (sum+target)%2==1){
return 0;
}
int[] dp=new int[bagSize+1];
dp[0]=1;
for (int i = 0; i < nums.length; i++) {
for (int j = bagSize; j >=nums[i] ; j--) {
dp[j]=dp[j]+dp[j-nums[i]];
}
}
return dp[bagSize];
}
1和0 当背包容量为指定值时最多能装几个物品?
int dp[][]=new int[m+1][n+1];
for (int i = 0; i < strs.length; i++) {
int zeroC=0,oneC=0;
for (int j = 0; j < strs[i].length(); j++) {
if (strs[i].charAt(j)=='0'){
zeroC++;
}else {
oneC++;
}
}
//遍历维度是必须
for (int j = m; j >=zeroC; j--) {
for (int k=n; k>=oneC; k--) {
dp[j][k]=Math.max(dp[j][k],dp[j-zeroC][k-oneC]+1);
}
}
}
return dp[m][n];
完全背包理论(物品可以无限次拿取)
和0-1背包的不同的是dp的递推公式,因为是无限次所以dp[i][j-weight[i],0-1背包中是dp[i-1][j-weight[i]]
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner scanner=new Scanner(System.in);
int n=scanner.nextInt();//材料种类
int v=scanner.nextInt();//行李承担的总重量
int[] weight=new int[n];
int[] value=new int[n];
for(int i=0;i<n;i++){
weight[i]=scanner.nextInt();
value[i]=scanner.nextInt();
}
int[][] dp=new int[n][v+1];
for(int j=weight[0];j<=v;j++){
dp[0][j]=dp[0][j-weight[0]]+value[0];
}
for(int i=1;i<n;i++){
for(int j=0;j<=v;j++){
if(j<weight[i]){
dp[i][j]=dp[i-1][j];
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);
}
}
}
System.out.println(dp[n-1][v]);
scanner.close();
}
}
总结:二维数组解决背包问题,物品和背包重量循环谁在外面都可以,且遍历顺序都是从前往后,因为要靠之前的结果得到后面的结果!!
零钱兑换II(完全背包的装满背包多少种方法-组合问题)
二维dp数组
public int change(int amount, int[] coins) {
int[][] dp=new int[coins.length][amount+1];
for(int j=0;j<=amount;j++){
if(j%coins[0]==0){
dp[0][j]=1;
}
}
for(int i=1;i<dp.length;i++){
for(int j=0;j<=amount;j++){
if(j<coins[i]){
dp[i][j]=dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]];
}
}
}
return dp[coins.length-1][amount];
}
一维dp数组
public int change(int amount, int[] coins) {
int[] dp=new int[amount+1];
dp[0]=1;
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
![]()
组合总和(规定不同顺序组合也算不同组合实质为排列问题)
public int combinationSum4(int[] nums, int target) {
int[] dp=new int[target+1];
dp[0]=1;
for(int j=0;j<=target;j++){
for(int i=0;i<nums.length;i++){
if(j>=nums[i]){
dp[j]=dp[j]+dp[j-nums[i]];
}
}
}
return dp[target];
}
总结:组合和排列问题在于物品和背包的内外顺序,组合问题是物品在外层循环,排列问题是背包在外层循环。
零钱兑换(背包装满最少个数)
除了dp[0]设置为0之外,其它初始值设置为最大值,只有当dp[j-coins[i]]不为最大值时,才有比较的意义。
public int coinChange(int[] coins, int amount) {
int[] dp=new int[amount+1];
for(int j=1;j<=amount;j++){
dp[j]=Integer.MAX_VALUE;
}
dp[0]=0;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
if (dp[j-coins[i]]!=Integer.MAX_VALUE){
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
}
if ( dp[amount]==Integer.MAX_VALUE){
return -1;
}
return dp[amount];
}
单词拆分(字符串的装满问题)⭐
首先,dp[j]的含义是当字符串的长度为j时,是否可以被字典填满
递推公式:当dp[j-str.length()]为true且子串和字典中的一个单词相同时,dp[j]=true;
遍历顺序:因为是排列问题,所以必须是先遍历背包再遍历物品。
本题字符串相当于背包,用字符串组去填拆分得到字符串。
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp=new boolean[s.length()+1];
dp[0]=true;
//当字符串在第j个位置时,是否能被字典完全拆分
for (int j = 1; j <=s.length() ; j++) {
//
for (int i = 0; i < wordDict.size(); i++) {
String str=wordDict.get(i);
if (j>=str.length() && dp[j-str.length()] && s.substring(j-str.length(),j).equals(str)){
dp[j]=true;
}
}
}
return dp[s.length()];
}
打家劫舍
先自己写递推过程,然后确定dp[j]的含义
dp[j]表示到了第j家偷盗的最高金额;
面临偷与不偷两个问题,取最大值,则递推公式为max(dp[j-2]+nums[j],dp[j-1]);
public int rob(int[] nums) {
int[] dp=new int[nums.length];
for(int i=0;i<nums.length;i++){
if(i==0){
dp[0]=nums[0];
}else if(i==1){
dp[1]=Math.max(nums[1],dp[0]);
}else{
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
}
return dp[nums.length-1];
}
打家劫舍II(第一家和最后一家不能一起打劫)
分成两个部分求最大值,分别是第0家到倒数第二家,第1家到倒数第一家。
其中,每个部分的dp需要重新分配内存,且index++和赋值不能写在一行,会出错,dp的下标用index,不用i;
public int rob(int[] nums) {
if(nums.length==1){
return nums[0];
}
return Math.max(robAction(nums,0,nums.length-1),robAction(nums,1,nums.length));
}
public int robAction(int[] nums,int start,int end){
int[] dp=new int[end-start];
int index=0;
for(int i=start;i<end;i++){
if(i==start){
dp[index++]=nums[start];
}else if(i==start+1){
dp[index++]=Math.max(dp[0],nums[i]);
}else{
dp[index]=Math.max(dp[index-2]+nums[i],dp[index-1]);
index++;
}
}
return dp[index-1];
}
打家劫舍III(二叉树形式)
dp的长度为2,dp[0]表示不偷该节点的最大金额,dp[1]表示偷该节点的最大金额;
需要使用递归,因为当前节点的值由后面的值决定。
public int rob(TreeNode root) {
int[] res = robAction(root);
return Math.max(res[0],res[1]);
}
public int[] robAction(TreeNode root){
int[] res=new int[2];
if (root==null){
return res;
}
int[] left=robAction(root.left);
int[] right=robAction(root.right);
//不偷当前节点
int leftMax=Math.max(left[0],left[1]);
int rightMax=Math.max(right[0],right[1]);
res[0]=leftMax+rightMax;
//偷当前节点
res[1]=left[0]+right[0]+root.val;
return res;
}
股票买卖的最佳时机(一次买卖次数)
双层for循环会运行超时
使用dp[i][0]表示第i天持有股票的时的最大金额,dp[i][1]表示第i天不持有股票的最大金额
dp[i][0]从前i-1天持有股票的最大金额和第i天 持有股票的最大金额中选最大值;
dp[i][1]前i-1天不持有股票的最大金额和第i天卖出股票的最大金额中选最大值。
public int maxProfit(int[] prices) {
int[][] dp=new int[prices.length][2];
dp[0][0]=-prices[0];
dp[0][1]=0;
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],-prices[i]);//第i天持有股票
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
return dp[prices.length-1][1];
}
股票买卖II(多次买卖)
将dp[i][0]的表达式进行改变,第i天买入该股票时的最大金额变为dp[i-1][1]-prices[i],原先是-prices[i]
public int maxProfit(int[] prices) {
int[][] dp=new int[prices.length][2];
dp[0][0]=-prices[0];
dp[0][1]=0;
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);//第i天持有股票
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
return dp[prices.length-1][1];
}
股票(最多买卖两次)
使用4个状态,分别是第一次/第二次持有股票/不持有股票。
public int maxProfit(int[] prices) {
int[][] dp=new int[prices.length][4];
dp[0][0]=-prices[0];
dp[0][1]=0;
dp[0][2]=-prices[0];
dp[0][3]=0;
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],-prices[i]);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]-prices[i]);
dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]+prices[i]);
}
return dp[prices.length-1][3];
}
股票(最多买卖K次)
public int maxProfit(int k, int[] prices) {
int[][] dp=new int[prices.length][2*k];
//初始化
for(int i=0;i<2*k;i++){
if(i%2==0){
dp[0][i]=-prices[0];
}else{
dp[0][i]=0;
}
}
for(int i=1;i<prices.length;i++){//第i天
for(int j=0;j<2*k;j++){//第i天的状态
if(j==0){
dp[i][j]=Math.max(dp[i-1][j],-prices[i]);
}else if(j%2==1){//不持有状态
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
}else{//持有状态
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
}
}
}
return dp[prices.length-1][2*k-1];
}
股票(多次买卖+冷冻期)
对于不合法状态,直接带入公式得到初始合理值。
dp[i][0]:第i天持有股票时的最大金额
dp[i][1]:第i天保持不持股(冷冻期之后)的最大金额
dp[i][2]:第i天卖出股时的最大金额
dp[i][3]:第i天为冷冻期的最大金额。
public int maxProfit(int[] prices) {
int[][] dp=new int[prices.length][4];
//初始化
dp[0][0]=-prices[0];
dp[0][1]=0;
dp[0][2]=0;
dp[0][3]=0;
for(int i=1;i<prices.length;i++){//第i天
int temp=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][0]=Math.max(temp,dp[i-1][3]-prices[i]);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][3]);
dp[i][2]=dp[i-1][0]+prices[i];
dp[i][3]=dp[i-1][2];
}
int temp=Math.max(dp[prices.length-1][1],dp[prices.length-1][2]);
return Math.max(temp,dp[prices.length-1][3]);
}
最长递增子序列
使用双层for循环,因为不能单纯根据dp[i-1]推断出结果
dp[i]表示以nums[i]为结尾数组的最长严格递增子序列的长度。
最后结果需要遍历dp数组,得到最大值。
public int lengthOfLIS(int[] nums) {
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
for(int i=1;i<nums.length;i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
}
int temp=1;
for(int i:dp){
temp=Math.max(i,temp);
}
return temp;
}
最长连续递增子序列
区别在于没有双层for循环
public int findLengthOfLCIS(int[] nums) {
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
for(int i=1;i<nums.length;i++){
if(nums[i]>nums[i-1]){
dp[i]=dp[i-1]+1;
}
}
int temp=1;
for(int i:dp){
temp=Math.max(i,temp);
}
return temp;
}
最长重复子数组
dp[i][j]表示以nums1[i]结尾的A和nums2[j]结尾的B,最长重复子数组(两个末尾数字必须相同,否则结果为0)

public int findLength(int[] nums1, int[] nums2) {
int[][] dp=new int[nums1.length][nums2.length];
int max=0;
for (int i = 0; i < nums1.length; i++) {
if (nums1[i]==nums2[0]){
dp[i][0]=1;
max=1;
}
}
for (int i = 0; i < nums2.length; i++) {
if (nums1[0]==nums2[i]){
dp[0][i]=1;
max=1;
}
}
for(int i=1;i<nums1.length;i++){
for(int j=1;j<nums2.length;j++){
if(nums1[i]==nums2[j]){
dp[i][j]=dp[i-1][j-1]+1;
max=Math.max(dp[i][j],max);
}
}
}
return max;
}
最长公共子序列(不连续)
该题就和上面不一样,后面的结果只会大于等于前面的结果
public int longiestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length() + 1][text2.length() + 1]; // 先对dp数组做初始化操作
for (int i = 1 ; i <= text1.length() ; i++) {
char char1 = text1.charAt(i - 1);
for (int j = 1; j <= text2.length(); j++) {
char char2 = text2.charAt(j - 1);
if (char1 == char2) { // 开始列出状态转移方程
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[text1.length()][text2.length()];
}
判断子序列
dp[i][j]表示以下标i-1结尾的字符串s,和以下标j-1结尾的字符串t,当s完全在t中时t的长度。
当子序列是ab 主序列是ah时,也判定为0

public boolean isSubsequence(String s, String t) {
int[][] dp=new int[s.length()+1][t.length()+1];
for(int i=1;i<=s.length();i++){
for(int j=1;j<=t.length();j++){
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=dp[i][j-1];
}
}
}
return dp[s.length()][t.length()]==s.length();
}
总结:关于不相等时,dp[i][j]=max(dp[i-1][j],dp[i][j-1])和dp[i][j]=dp[i][j-1]的区别在于,要不要求连续,两个都不连续时使用max,当有同一个要求连续时,使用dp[i][j]=dp[i][j-1]。
不同的子序列⭐
| b | a | e | g | g | ||
| 1 | 1 | 1 | 1 | 1 | 1 | |
| b | 0 | 1 | 1 | 1 | 1 | 1 |
| a | 0 | 0 | 1 | 1 | 1 | 1 |
| g | 0 | 0 | 0 | 0 | 1 | 2 |
dp[i][j]分为两个部分,分别是使用t[j-1]和不使用t[j-1],当使用t[j-1]时dp[i][j]=dp[i-1][j-1],不使用时,dp[i][j]=dp[i][j-1]。
public int numDistinct(String s, String t) {
int[][] dp=new int[t.length()+1][s.length()+1];
for(int j=0;j<=s.length();j++){
dp[0][j]=1;
}
for(int i=1;i<=t.length();i++){
char t1=t.charAt(i-1);
for(int j=1;j<=s.length();j++){
char s1=s.charAt(j-1);
if(t1==s1){
dp[i][j]=dp[i-1][j-1]+dp[i][j-1];
}else{
dp[i][j]=dp[i][j-1];
}
}
}
return dp[t.length()][s.length()];
}
两个字符串的删除操作
dp数组如下图所示:
| e | a | t | ||
| 0 | 1 | 2 | 3 | |
| s | 1 | 2 | 3 | 4 |
| e | 2 | 1 | 2 | 3 |
| a | 3 | 3 | 2 | 1 |
public int minDistance(String word1, String word2) {
int[][] dp=new int[word1.length()+1][word2.length()+1];
for(int i=0;i<=word1.length();i++){
dp[i][0]=i;
}
for(int j=0;j<=word2.length();j++){
dp[0][j]=j;
}
for(int i=1;i<=word1.length();i++){
char w1=word1.charAt(i-1);
for(int j=1;j<=word2.length();j++){
char w2=word2.charAt(j-1);
if(w1==w2){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(dp[i-1][j]+1,dp[i][j-1]+1);
}
}
}
return dp[word1.length()][word2.length()];
}
编辑距离(比上题多了一个删除操作)
递归表达式不相等情况多了个dp[i-1][j-1]+1
| e | x | e | c | u | t | i | o | n | ||
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
| i | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 6 | 7 | 8 |
| n | 2 | 2 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 8 |
public int minDistance(String word1, String word2) {
int[][] dp=new int[word1.length()+1][word2.length()+1];
for(int i=0;i<=word1.length();i++){
dp[i][0]=i;
}
for(int j=0;j<=word2.length();j++){
dp[0][j]=j;
}
for(int i=1;i<=word1.length();i++){
char w1=word1.charAt(i-1);
for(int j=1;j<=word2.length();j++){
char w2=word2.charAt(j-1);
if(w1==w2){
dp[i][j]=dp[i-1][j-1];
}else{
int temp=Math.min(dp[i-1][j]+1,dp[i][j-1]+1);
dp[i][j]=Math.min(temp,dp[i-1][j-1]+1);
}
}
}
return dp[word1.length()][word2.length()];
}
回文子串
dp[i][j]表示以i开始j结尾的字符串是否为回文子串,true/fasle
根据dp表格得,初始化先将对角线原始初始化为true(也可以在遍历的时候直接判断),然后遍历顺序是从中间到右边,从下面到上面
| c | b | a | b | c | |
| c | T | f | f | f | T(3) |
| b | T | f | T(2) | f | |
| a | T(1) | f | f | ||
| b | T | f | |||
| c | T |
public int countSubstrings(String s) {
int result=0;
boolean[][] dp=new boolean[s.length()][s.length()];
for(int i=s.length()-1;i>=0;i--){
char c1=s.charAt(i);
for(int j=i;j<s.length();j++){
char c2=s.charAt(j);
if(c1==c2){
if(j-i<=1){
dp[i][j]=true;
}else{
dp[i][j]=dp[i+1][j-1];
}
if(dp[i][j]){
result++;
}
}
}
}
return result;
}
最长回文子序列
dp[i][j]表示以i开始,j结束的最长的回文子序列个数
对于aab当开头和末尾不相等时,从aa/ab中取最长回文子序列值
对于abaa,当开头和末尾相等时,比较aba/ba+2取最大值
| a | a | b | a | a | |
| a | 1 | 2 | 2 | 3 | 5 |
| a | 1 | 1 | 3 | 3 | |
| b | 1 | 1 | 2 | ||
| a | 1 | 2 | |||
| a | 1 |
public int longestPalindromeSubseq(String s) {
int[][] dp=new int[s.length()][s.length()];
int max=1;
for(int i=s.length()-1;i>=0;i--){
char s1=s.charAt(i);
for(int j=i;j<s.length();j++){
char s2=s.charAt(j);
if(s1==s2){
if(i==j){
dp[i][j]=1;
}else{
dp[i][j]=Math.max(dp[i][j-1],dp[i+1][j-1]+2);
}
}else{
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
max=Math.max(max,dp[i][j]);
}
}
return max;
}


浙公网安备 33010602011771号