LeetCode 292. Nim 游戏(博弈) / 650. 只有两个键的键盘 / 673. 最长递增子序列的个数(LIS二分+前缀和) / 58. 最后一个单词的长度

292. Nim 游戏

2021.9.18 每日一题

题目描述

你和你的朋友,两个人一起玩 Nim 游戏:

桌子上有一堆石头。
你们轮流进行自己的回合,你作为先手。
每一回合,轮到的人拿掉 1 - 3 块石头。
拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。

示例 1:

输入:n = 4
输出:false
解释:如果堆中有 4 块石头,那么你永远不会赢得比赛;
因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走。

示例 2:

输入:n = 1
输出:true

示例 3:

输入:n = 2
输出:true

提示:

1 <= n <= 2^31 - 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/nim-game
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

其实就是一个想法
如果只有三个石子,那么先手肯定胜利,这个不用多说
如果有四个,那么不管先手拿几个,后手肯定都能拿完,必败
那么继续想,如果有5-7个,那么先手就拿1-3个,剩下四个,那么后手的人成了“之前的先手”,必败,所以先手必胜
8个,又是不管拿几个,后手的人会让先手陷入“4的魔咒”,然后就输了…
所以,往后分析也是一样,在不是4的倍数时,先手可以让后手陷入“4的魔咒”;而是4的倍数时,先手本身就在4的魔咒中,所以必败

当然,这只是这个题,博弈的题我觉得非常难,比动态规划还要难,难就难在每个人都是最优解,有时候很不好想,也不好写

所以看三叶姐的总结方法,首先举例子找规律,然后分析什么条件下是先手必胜的,这种角度来分析问题,而得到答案

class Solution {
    public boolean canWinNim(int n) {
        //另一个数学规律就是,4个一轮回,4的倍数就是false,否则就是true
        if(n % 4 == 0) 
            return false;
        else 
            return true;
    }
}

650. 只有两个键的键盘

2021.9.19 每日一题

题目描述

最初记事本上只有一个字符 ‘A’ 。你每次可以对这个记事本进行两种操作:

Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。
Paste(粘贴):粘贴 上一次 复制的字符。
给你一个数字 n ,你需要使用最少的操作次数,在记事本上输出 恰好 n 个 ‘A’ 。返回能够打印出 n 个 ‘A’ 的最少操作次数。

示例 1:

输入:3
输出:3
解释:
最初, 只有一个字符 ‘A’。
第 1 步, 使用 Copy All 操作。
第 2 步, 使用 Paste 操作来获得 ‘AA’。
第 3 步, 使用 Paste 操作来获得 ‘AAA’。

示例 2:

输入:n = 1
输出:0

提示:

1 <= n <= 1000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/2-keys-keyboard
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

发现质数都是一个一个复制而来的,合数可以由质数复制得到
所以遍历一个合数的因子,动态规划得到结果

class Solution {
    public int minSteps(int n) {
        //想想怎么做
        //好像跟质数有点关系,如果是质数的话,就只能一个个复制
        //如果是合数,例如6可以通过3,8通过4,4通过2,
        //20通过10,10通过5,;20也可以通过4,4通过2,
        //也就是说合数可以通过质数来得到,
        
        //dp表示能形成n个字符的操作次数,j是n的因子
        //dp[n] = dp[n / j] * (n / j - 1) + 1;
        int[] dp = new int[n + 1];
        dp[1] = 0;
        for(int i = 2; i <= n; i++){
            dp[i] = Integer.MAX_VALUE;
            int j = 1;
            while(j * j <= i){
                int count = i / j;
                if(count * j != i){
                    j++;
                    continue;
                }  
                //复制1次,粘贴count-1次,
                dp[i] = Math.min(dp[i], dp[j] + count);
                dp[i] = Math.min(dp[i], dp[count] + j);
                j++;
            }
        }
        return dp[n];
    }
}

这个数学的方法怎么想呢,从后向前,如果当前要求的n,n可以由k1个j1复制而来,也就是n = k1 * j1
那么j1又可以由k2个j2复制而来,所以n = k1 * k2 * j2…
也就是说,n是由一堆因子的乘积而得到的
所以其实只需要求出这几个因子就行了
但是又很容易想到一个问题,就是这样的因子乘积是不是唯一的,答案应该是,是唯一的
因为最终都会变成质数的乘积,而质数是不能拆分的

class Solution {
    public int minSteps(int n) {
        //分解质因数
        
        int res = 0;
        //遍历所有可能的因子
        for(int i = 2; i * i <= n; i++){
            //如果可以除尽,说明是一个因子,但是这个因子可能还能除
            while(n % i == 0){
                n /= i;
                //copy一次,粘贴i - 1次
                res += i;
            }
        }
        //最后没有因子的就是质数了
        if(n > 1)
            res += n;
        return res;
    }
}

673. 最长递增子序列的个数

2021.9.20 每日一题

题目描述

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 1:

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

示例 2:

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

我的思路就是,在动态规划求最长递增子序列长度的同时,求个数
和求最长递增子序列长度一样,定义dp[i]为以i为结尾的最长递增子序列长度
然后用动态规划的方法求解,在求解的过程中,会遍历前面所有位置,如果有一个位置能使当前位置的长度变长,那么就设置该长度并且统计个数;如果有位置的长度加上当前位置,和最长长度相同,也补充到最长的个数上
因为求的是最长递增子序列的个数,所以用一个遍历记录当前最长的长度,用一个变量记录当前最长长度的个数
On2的复杂度,勉强通过

class Solution {
    public int findNumberOfLIS(int[] nums) {
        int l = nums.length;

        int[] dp = new int[l];      //以nums[i]结尾的长度
        int[] count = new int[l];   //个数
        int max = 1;
        int res = 0;
        for(int i = 0; i < l; i++){
            dp[i] = 1;
            count[i] = 1;
            for(int j = 0; j < i; j++){
                //如果大于,那么子序列长度增长,更新长度和数量
                if(nums[j] < nums[i] && dp[j] + 1 > dp[i]){
                    dp[i] = dp[j] + 1;
                    count[i] = count[j];
                //如果等于,那么数量增加
                }else if(dp[j] == dp[i] - 1){
                    count[i] += count[j];
                }
            } 
            if(dp[i] > max){
                max = dp[i];
                res = count[i];
            }else if(dp[i] == max){
                res += count[i];
            }
        }
        
        return res;
    }
}

想想二分那种求最长递增子序列的方法能写吗
回顾了一下这个最长递增子序列的二分写法,是贪心使得每个位置都是最小值,这样就可以最大程度的增大长度
那么这个题呢,需要记录个数,而二分的时候,如果要插入一个值num,只是找到第一个大于等于当前数num的位置 j ,然后把这个位置上的数减小,dp[j] = num;
而要统计这个值对应的序列长度,那么就需要找到前一个位置dp[j - 1],同时要找到序列长度为j - 1,并且最后一个数小于num的数量,最后count[j][num] += dp[j - 1][小于num的第一个位置]
所以需要将dp变成一个二维的数组,记录每个位置结尾的数
而由于插入的时候,是小于等于dp[j]末尾值才插入当前位置dp[j],所以肯定是插入到最后,所以dp[j]是单调非增的

class Solution {
    public int findNumberOfLIS(int[] nums) {
        //想想二分
        //每次找到的都是当前值该插入的位置
        //那么应该记录什么呢,记录当前长度递增子序列的个数
        //如果找到一个位置,那么这个位置的个数就count[i - 1]
        //但是有可能出现后面扩展的长度的数是比当前之前长度的数小的,比如 1 3 8 4 7在扩展7时
        //所以还需要记录每一个位置,改变时,长度的个数
        //描述起来比较抽象,而且我觉得这样做好像并不能加快多少...
        //看了一眼答案,还果然是这样写的。。
        int l = nums.length;
        List<List<Integer>> dp = new ArrayList<>();     //记录当前长度i下,可以出现的末尾的值
        List<List<Integer>> count = new ArrayList<>();  //记录当前长度i,并且末尾值为dp[i][idx]的子序列个数,保存的是前缀和

        for(int num : nums){
            //首先找该插入的位置
            int left = 0;
            int right = dp.size();
            while(left < right){
                int mid = (right - left) / 2 + left;
                List<Integer> temp = dp.get(mid);
                //找第一个比num大的
                //如果最后一个位置的值,小于num,说明当前位置不行
                if(temp.get(temp.size() - 1) < num){
                    left = mid + 1;
                }else{
                    right = mid;
                }
            }
            //找到了要插入的序列位置,这个位置为left
            //首先想一下这个list的性质,因为前一个二分是用temp的最后一个值比较的,所以比当前小的值才能插入
            //而插入的话,肯定是在最后一个位置,所以list是非增的
            //那么插入位置确定以后,要找的就是数量,而数量是统计前一个位置,即left - 1序列中小于num的个数
            int c = 1;
            if(left > 0){
                //cur是单调递减的,并且要找的是第一个小于num的位置,这样的话小于的部分就可以扩展num
                List<Integer> cur = dp.get(left - 1);
                int ll = 0;
                int rr = cur.size();
                while(ll < rr){
                    int mid = (rr - ll) / 2 + ll;
                    //如果满足条件,那么说明rr可行
                    if(cur.get(mid) < num){
                        rr = mid;
                    }else{
                        //如果大于等于,那么就需要往右边找
                        ll = mid + 1;
                    }
                }
                //这里找到了第一个小于num的位置,那么数量就等于
                List<Integer> cnt = count.get(left - 1);
                //因为保存的是前缀和,所以是这样算
                c = cnt.get(cnt.size() - 1) - cnt.get(ll);
            }
            //如果要插入最后一个位置
            if(left == dp.size()){
                List<Integer> dd = new ArrayList<>();
                dd.add(num);
                dp.add(dd);
                List<Integer> cc = new ArrayList<>();
                //因为放的是前缀和,所以补0
                cc.add(0);
                cc.add(c);            
                count.add(cc);
            }else{
                dp.get(left).add(num);
                List<Integer> cc = count.get(left);
                int pre = cc.get(cc.size() - 1) + c;
                cc.add(pre);
            }
        }
        //最后
        int size1 = count.size();   //最长的长度
        int size2 = count.get(size1 - 1).size();    //最长长度的最后一个值
        return count.get(size1 - 1).get(size2 - 1);

    }
}

58. 最后一个单词的长度

2021.9.21 每日一题

题目描述

给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

示例 1:

输入:s = “Hello World”
输出:5

示例 2:

输入:s = " fly me to the moon "
输出:4

示例 3:

输入:s = “luffy is still joyboy”
输出:6

提示:

1 <= s.length <= 10^4
s 仅有英文字母和空格 ’ ’ 组成
s 中至少存在一个单词

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/length-of-last-word
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

就不写从后向前遍历的代码了,简单题,追求的是速度

class Solution {
    public int lengthOfLastWord(String s) {
        s = s.trim();
        String[] ss = s.split(" ");
        return ss[ss.length - 1].length();
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值