线性DP
俺的理解就是给定一个数组,遍历过程中不断向前增大dp的范围,感觉有点像01背包,不过消耗是变长的。
这道题,与什么最大和啊啥的区别就在于他的比较的不是数组本身的子数组,而是自己另外一个数组,解决思路,还是归结到选与不选的问题上来,子问题就是s2子序列匹配s1串的子序列,到最小的子序列肯定一个字符啦。
dp数组的定义:
dp[i][j]的话,就是到i的s2子串有的子序列匹配到j为止的子序列最长匹配多少。(我这里写的优化了下空间,意义一样的,因为每次只需要用到i-1,j或者i,j-1位置的数据)
转移方程:
如果新字符可以匹配上,那么就是上一个子串在该位置匹配时的最大长度+1,
如果不能,就分两种情况,1.看是自己之前匹配上最长串为多大2.上一个串在该处时最长长度,取较大者。
dp[i][j] = if 匹配上,dp[i-1][j]+1 else max(dp[i][j-1],dp[i-1][j]);
(这时dp数组的优化就好想了,只需要将上次的dp[j]位置保存下来,就不怕dp[j]被覆盖了)
‘’ | a | b | a | |
---|---|---|---|---|
‘’ | 0 | 0 | 0 | 0 |
a | 0 | 1 | 1 | 1 |
b | 0 | 1 | 2 | 2 |
d | 0 | 1 | 2 | 2 |
c | 0 | 1 | 2 | 2 |
a | 0 | 1 | 2 | 2 |
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int[] dp= new int[text1.length()+1];
for(int j=0;j<text2.length();j++){
int last = 0;
for(int i=0;i<text1.length();i++){
int temp = dp[i+1];
if(text2.charAt(j)==text1.charAt(i)){
dp[i+1] = last+1;
}
else{
dp[i+1] = Math.max(dp[i],temp);
}
last=temp;
}
}
return dp[text1.length()];
}
}
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length+1];
int count = 0;
dp[0] = Integer.MIN_VALUE;
for(int i=0;i<nums.length;i++){
if(dp[count]<nums[i]){
dp[++count] = nums[i];
}
else{
int left= 1,right=count;
while(left<right){
int mid = (right+left)>>1;
if(nums[i]>dp[mid]){
left = mid+1;
}else{
right = mid;
}
}
dp[left] = nums[i];
}
}
return count;
}
}
画画图就会发现,上一层(i)和下一层(j)的到达的索引关系为i-> i,i+1 j->j-1,j.
dp[i][j] 的含义,到达第i,j位置最短的路径。
转移方程:dp[i][j] = Min(dp[i-1][j],dp[i-1][j-1])+nums[i][j];
优化方式dp数组与子序列那题类似,只需保存dp[j]位置的值即可,防止覆盖
不过初始化需要注意一下,因为转移方程在0和length-1位置会变为只有一个方向。
//自顶向下,O(n)空间复杂度
class Solution {
public static int minimumTotal(List<List<Integer>> triangle) {
int size = triangle.size();
int[] dp = new int[size];
dp[0] = triangle.get(0).get(0);
for (int i = 1; i < size; i++) {
List<Integer> list = triangle.get(i);
int pre = dp[0];
dp[0] += list.get(0);
for (int j = 1; j < list.size() - 1; j++) {
int temp = dp[j];
dp[j] = Math.min(pre, dp[j]) + list.get(j);
pre = temp;
}
if(list.size()!=1) {
dp[list.size() - 1] = pre + list.get(list.size() - 1);
}
}
int min = dp[0];
for (int i = 1; i < size; i++) {
min = Math.min(min, dp[i]);
}
return min;
}
}
用贪心的思想,如果和还是正数就加上它,如果加起来总和为负了就一点益处都没有了,前面的全部抛弃,再用一个值来记录每个情况下的最大值即可。
动规的话(本质就是多次局部贪心嘛)
class Solution {
public int maxSubArray(int[] nums) {
int max = nums[0],pre = nums[0];
for(int i =1;i<nums.length;i++){
pre = Math.max(pre,0)+nums[i];
max = Math.max(pre,max);
}
return max;
}
}
这个题与上一题的区别就在于需要考虑值为负数时,虽然此时为最小,但是可能之后再来个负数就为最大了,所以我们需要记录一下,最小值(负情况的最大值)和最大值。
在遇到负数时,最大值和最小值交换。
class Solution {
public int maxProduct(int[] nums) {
int max=nums[0],tmin=nums[0],tmax=nums[0];
for(int i=1;i<nums.length;i++){
if(nums[i]<0){
int temp = tmax;
tmax = tmin;
tmin = temp;
}
tmax = Math.max(tmax*nums[i],nums[i]);
tmin = Math.min(tmin*nums[i],nums[i]);
max = Math.max(tmax,max);
}
return max;
}
}
这道题差点把我送走,俺知道它难,没想到这么难。
首先暴力法,要知道能测出每一层楼(0~f)时的最小操作数,子问题就是,要我们选择的就是在哪一楼扔蛋(还有n个)最优,而在x楼扔都面临两个选择,一个蛋碎了,那么可能就是x-1楼,我们还剩n-1个蛋,如果没碎,那么可能就是f-x楼,剩n个蛋,因为要肯定能够全部试出来,所以我们选择次数多的,好了,可以暴力了,深搜来吧。
//暴力没过,能过1/4的数据
class Solution {
public int superEggDrop(int K, int N) {
//System.out.println(K+" "+N);
if(K==1||N==1||N==0){
return N;
}
int min = N;
for(int i= 1;i<N;i++){
int max = Math.max(superEggDrop(K-1,i-1),superEggDrop(K,N-i));
min = Math.min(max+1,min);
}
return min;
}
}
可想而知,计算了很多重复的情况,每个都需要计算到底层。那么我们就把计算过的保存一下吧。
//超时了,能过1/2的数据
class Solution {
int[][] dp;
public int superEggDrop(int K,int N){
dp = new int[N+1][K+1];
Object[] dd =
Arrays.stream(dp).map(x->{
return Arrays.stream(x).map(y->{ y =Integer.MAX_VALUE;return Integer.valueOf (y);}).toArray();
}).toArray();
System.arraycopy(dd, 0, dp, 0, dd.length);
return superEggDropIn(K,N);
}
private int superEggDropIn(int K, int N) {
//System.out.println(K+" "+N);
if(K==1||N==1||N==0){
return N;
}
if(dp[N][K]!=Integer.MAX_VALUE){
return dp[N][K];
}
int min = N;
for(int i= 1;i<N;i++){
int max = Math.max(superEggDropIn(K-1,i-1),superEggDropIn(K,N-i));
min = Math.min(max+1,min);
}
dp[N][K] = min;
return min;
}
啊还是用常规动规刷表的方式写写吧(主要是看了答案,深搜没办法后面的二分优化)
设想一下,dp数据该怎么写(dp选得好,用时就会少)。
按照刚才深搜的思路,dp数组应该为dp[i][j]在i层楼时,有j个蛋,这时最少的扔鸡蛋的次数为多少。
转移方程,从很明显就是选从1~i层楼分别作为起始点,最小的操作数啊,
dp[i][j] = Min(dp[0~i][?])
不完整,蛋碎不碎分情况的啊得,在某一层楼时,如果碎了和没碎可是有两种情况需要考虑的,因为要把每一层楼尝试一遍,所以取两者中的较大者
dp[i][j] = Min(Max(dp[0i][j-1],dp[0i][j]))
规整规整,
dp[i][j] = Mini1 (Max(dp[k][j-1],dp[i-k][j]));
初始化:在0楼肯定0,一个蛋肯定N,一楼肯定1
//超时+1,2/3的数据
class Solution {
public int superEggDrop(int K, int N) {
int[][] dp = new int[N+1][K+1];
Arrays.fill(dp[0],0);
Arrays.fill(dp[1],1);
// dp = (int[][]) Arrays.stream(dp).map((x)-> x[0]=0
// ).toArray();
for(int i=0;i<dp.length;i++){
dp[i][0] = 0;
dp[i][1] = i;
}
for(int i=1;i<=N;i++)
for(int j=2;j<=K;j++) {
dp[i][j] = i;
for (int k = 1; k <= i; k++) {
dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - k][j], dp[k- 1][j - 1]) + 1);
}
}
return dp[N][K];
}
}
能把俺逼到这个程度优化的也只有这道题了(做的其他题简单嘛,能过就行了 (滑稽))
用二分法优化,一直用二分做搜索的我,看到这个说法是有点懵的,不过看了也是好理解,能用二分优化本质上是序列成单调递增或者单调递减,而这道题,我们稍作思考 (锤子个,琢磨了老久),这个本质上是求一个F(K,N)函数,求它的极值点,可想而知当K不变时,随着楼层的增加,F自然会增大,我们的两种情况可以写为F1(K-1,i-1),F2(K,N-i),如果i为自变量,可以轻易发现,两个操作,一个单增一个单减,它们的交点(或许有多个,不过都在一起的)的i便是我们需要求得的最优扔蛋楼层,该楼情况下操作数便是最优扔蛋次数。
那么如果F1>F2时交点肯定在当前楼层的楼上,如果F1>F2肯定在当前楼层的楼下。
class Solution {
public int superEggDrop(int K, int N) {
int[][] dp = new int[N+1][K+1];
Arrays.fill(dp[0],0);
Arrays.fill(dp[1],1);
// dp = (int[][]) Arrays.stream(dp).map((x)-> x[0]=0
// ).toArray();
for(int i=0;i<dp.length;i++){
dp[i][0] = 0;
dp[i][1] = i;
}
for(int i=2;i<=N;i++)
for(int j=2;j<=K;j++) {
int left = 1;
int right = i;
while (left < right) {
int mid = left + (right - left + 1) / 2;
int breakCount = dp[mid - 1][j - 1];
int notBreakCount = dp[i - mid][j];
if (breakCount > notBreakCount) {
right = mid - 1;
} else {
left = mid;
}
}
dp[i][j] = Math.max(dp[left - 1][j - 1], dp[i - left][j]) + 1;
}
return dp[N][K];
}
}
这种思路太秀了吧,逆向思维,题目要我们求N楼K蛋,操作多少次,那我们是不是可以求K蛋F次操作最多能到几楼,当楼层大于等于N时,就是我们要求的了。
dp数组定义如上所说。
转移方程:要想知道到达多少次操作最多能到几楼,它是不是由蛋碎了,蛋-1,操作数-1能确定的楼层数,蛋没碎操作数减1能确定的楼层数,加上本楼层被测试了的总楼层嘛
dp[i][j] = dp[i][j-1],dp[i-1][j-1]+1;
优化一下空间,就是
dp[j] = pre+dp[j]+1;
class Solution {
public int superEggDrop(int K, int N) {
int[] dp = new int[K+1];
int m = 0;
while (dp[K] < N) {
m++;
int pre =dp[0];
for (int k = 1; k <= K; k++){
int temp = dp[k];
dp[k] = dp[k] + pre + 1;
pre = temp;
}
}
return m;
}
}
这道题思路与最长子序列类似,不过变成了二维,那么可以先将其中一维排序,再对二维做求最大子序列。
class Solution {
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes,(x,y)->{
if(x[0]!=y[0]){
return x[0]-y[0];
}else{
return y[1]-x[1];
}
});
//排序之后根据高度求长子序列
int[] height = Arrays.stream(envelopes).mapToInt(x->x[1]).toArray();
return lengthOfLIS(height);
}
private int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length+1];
int count = 0;
dp[0] = Integer.MIN_VALUE;
for(int i=0;i<nums.length;i++){
if(dp[count]<nums[i]){
dp[++count] = nums[i];
}
else{
int left= 1,right=count;
while(left<right){
int mid = (right+left)>>1;
if(nums[i]>dp[mid]){
left = mid+1;
}else{
right = mid;
}
}
dp[left] = nums[i];
}
}
return count;
}
}
还是立足于偷还是不偷的问题,如果偷,那就需要前前个房间的最大值,如果不偷就要前一个房间的最大值,再从两者判断到底偷还是不偷。
class Solution {
public int rob(int[] nums) {
int pre=0,curr=0;
for(int i=0;i<nums.length;i++){
int temp = curr;
curr = Math.max(nums[i]+pre,curr);
pre = temp;
}
return curr;
}
}
可以将环拆开,拆成0~n-1,1 ~ n,退化为上一个问题。
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len==1){
return nums[0];
}
return Math.max(rob(nums,0,len-1),rob(nums,1,len));
}
public int rob(int[] nums,int start,int end) {
int pre=0,curr=0;
for(int i=start;i<end;i++){
int temp = curr;
curr = Math.max(nums[i]+pre,curr);
pre = temp;
}
return curr;
}
}