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();
}
}