TAG

题解

21 篇文章

题解-通过操作使数组长度最小筑-脑筋急转弯,用小数踢掉大数

Problem: 100164. 通过操作使数组长度最小 思路 观察$nums[i]%nums[j]$有如下的可能性 如果$nums[i]<nums[j]$,最后只是将数组中的$nums[j]$踢出 $nums[i]>=nums[j]$则是二者踢出,$nums[i]%nums[j]$进入数组,且显然$nums[i]%nums[j]<nums[j]<=nums[i]$ 因为没有限定ij的关系,发现这种操作要么是小数将大数从数组中踢掉,要么是引入一个更小的数,将二者踢掉 最后剩下的长度应该和数组中最小值有关,且这个最小值包括操作中可能产生的 如果没有产生新的最小值,则很容易知道最后剩下的数就是原始数组中最小值的数量,然后将其两两组合,引进0,就能得到答案 如何产生更小值: 回顾1,我们将所有数与最小值相模 如果余数不为0,就为新的最小值 如果余数全为0,说明所有数都是最小值的倍数,则它们之间两两相模的最小值大于等于原始数组的最小值 我们只需要产生一个新的更小值就能把所有其他数都踢掉,此时答案是1 时间复杂度:$O(n)$ Code class Solution: def minimumArrayLength(self, nums: List[int]) -> int: m=min(nums) for num in nums: if num%m!=0:# 可以产生新的最小值 return 1 length=nums.count(m) return length//2+length%2

↗

题解-求和游戏-数学,以及简单分类证明

Problem: 1927. 求和游戏 初始思路 考虑一些等价:我们先把这个数分为两个部分$left,right$,分别是左右两边数字的和,形式化写为$\displaystyle right=\sum_{i=n/2}^{n}num_i(0<=num_i<=9)$剩下的问号数量分别为$n_{left},n_{right}$ 我们要做的实际上是对$left和right$分别操作$n_{left},n_{right}$n次,其中我们允许的操作就是对该数加上0-9的任意一个数 显然操作是对称的,所以我们只考虑有效的操作数,也就是$abs(n_{left}-n_{right})$,而且显然有效的操作数只会存在于一个数身上:如果出现在两个身上就会对称消除 我们可以先除去一些情况: 如果剩下的有效操作是奇数,则Alice一定赢,因为她一定不会选择让数相等的操作(只有一个选择),而Alice有10个合法选择 然后我们发现操作一定会让数增加,也就是说,如果存在有效操作的是较大的那个数,则一定不可能相等 现在我们只剩下了一种情况:有一个较小的数能够让双方都操作$x$次,如何让较小的数变为较大的数 设两数相差的绝对值为$gap$,每个人能操作$x$次 Alice有两个选择: 一个是积极加,在自己的回合里都加9,这样会让较小的数飞快增长,如果两数相差比较小,就能让这个数超过大点的数,从而Bob不能选出来:也就是$9x>gap$,Alice能赢 另一个是完全消极,每次都加0,这样会让数缓慢增长,单靠Bob变数增加,如果$gap>9x$,此时Alice也赢 我们惊讶地发现此时出现了9x大于小于gap,Bob都会输,就只剩下了$$9x==gap$$,所幸,我们能发现此时Bob会赢,因为不管此时Alice如何操作,Bob后手都能补齐9 Code class Solution: def sumGame(self, num: str) -> bool: n=len(num) nums=0 left=0 right=0 for i in range(n//2): if num[i]=='?': nums+=1 else: left+=ord(num[i])-ord('0') for i in range(n//2,n): if num[i]=='?': nums-=1 else: right+=ord(num[i])-ord('0') if nums%2!=0 or (left>right and nums>0) or (left<right and nums<0): #最后轮到Alice或者一定不能赢的情况 return True AliceCan=abs(nums)//2 want=abs(left-right) if AliceCan*9!=want: return True return False

↗

题解-找出叠涂元素-哈希表,两种思路,在途中检查和在最后检查

Problem: 2661. 找出叠涂元素 思路:中途检查 解法 数据量不大,看着可以在每次涂色时候查询是否合法 但是每次都查询显然会超时,我们选择一个更有效的优化 已经说明各个数各不相同,不会出现一个格子被涂两遍的情况 我们用数组来记录每一行和每一列已经被涂的格子数量,就能知道这一行列是否被全涂满 时间复杂度: $O(m*n)$ 空间复杂度: $O(m*n)$ Code class Solution: def firstCompleteIndex(self, arr: List[int], mat: List[List[int]]) -> int: matMap={} m=len(mat) n=len(mat[0]) row=[0]*m col=[0]*n for i in range(m): for j in range(n): matMap[mat[i][j]]=(i,j) for index in range(len(arr)): num=arr[index] x,y=matMap[num] row[x]+=1 col[y]+=1 if row[x]==n or col[y]==m: return index return -1 思路:最后查询 解法 我们注意到当我们知道某一行的所有元素的时候,变相知道了这一行会被什么时候涂满,既这一行存在arr中最靠后的那个索引 所以我们的任务变成了寻找最先被涂满的行或者列 对arr进行hash,记录每个元素的具体位置,再遍历行列,在遍历过程中 行列取最大值表示让这一行填满最少需要的步数 res取上述的数的最小值 时间复杂度: $O(mn)$ 空间复杂度: $O(mn)$ Code class Solution { public: int firstCompleteIndex(vector<int>& arr, vector<vector<int>>& mat) { int res=arr.size(); unordered_map<int,int>hash; for (int i=0;i<arr.size();i++){ hash[arr[i]]=i; } for (int i=0;i<mat.size();i++){ int tmp=0; for (int j=0;j<mat[0].size();j++){ tmp=max(tmp,hash[mat[i][j]]); } res=min(res,tmp); } for (int j=0;j<mat[0].size();j++){ int tmp=0; for (int i=0;i<mat.size();i++){ tmp=max(tmp,hash[mat[i][j]]); } res=min(res,tmp); } return res; } }; class Solution: def firstCompleteIndex(self, arr: List[int], mat: List[List[int]]) -> int: res=len(arr) m=len(mat) n=len(mat[0]) arrLocation={} for i,a in enumerate(arr): arrLocation[a]=i for i in range(m): nowColNeedSteps=0 for j in range(n): nowColNeedSteps=max(nowColNeedSteps,arrLocation[mat[i][j]]) res=min(res,nowColNeedSteps) for j in range(n): nowRowNeedSteps=0 for i in range(m): nowRowNeedSteps=max(nowRowNeedSteps,arrLocation[mat[i][j]]) res=min(res,nowRowNeedSteps) return res

↗

题解-做菜顺序-排序,贪心,前缀和,一点点证明

Problem: 1402. 做菜顺序 思路 注意到没有要求顺序,先排序 朴素思想是将大的数越往后放越好,但对于负数,虽然也是越大越往后放越好,但其有可能增加答案,也有可能减少答案,需要一些证明 解题方法 注意到对于负数的选取一定是连续而非跳跃的:假设存在$[a_{i-1},a_i,a_{i+1}].a_i<a_{i+1},$数组,三者均小于0,假如一定要取两个元素,(这种假定的含义是我需要后面的正数有足够的倍数,期望前面提供倍数的负数(代价)最小),选取元素$a_{i-1},a_{i+1}$代价大于选取$a_i,a_{i+1}$ 推广可知我们会选取所有的正数和部分负数,且负数在排序后的数组中是连续的,并且和正数部分相连 实际上是求一个子数组的这个操作最大能多大,可以枚举每一次从哪选,结束一定是在最大的数结束 然后简化:注意到相关操作实际上是对于之前结果再加上一次已经遍历的数,所以可以将其转化为一遍扫描的方式 时间复杂度, $O(nlogn)$ Code class Solution: def maxSatisfaction(self, satisfaction: List[int]) -> int: satisfaction.sort(reverse=True) #方便操作 res=0 pre=0 now=0 for num in satisfaction: pre+=num now+=pre res=max(now,res) if now<0: break #之后一定小于0 return res

↗

题解-移动机器人-数组计算的三种思考方式方式(动态规划,贡献,前缀和)与相关证明

Problem: 2731. 移动机器人 前述条件 碰撞和1503. 所有蚂蚁掉下来前的最后一刻类似,注意到对于身份没有要求,所以可以直接认为两个机器人碰撞是交换了二者的身份,也可以认为是穿过去了 因此能够得到机器人的最终位置,为了方便计算,我们排序 数据量为$10^5$,暴力求解不现实 以下是三种常用的解决方法,这三种思路最开始的进入方式不同,但是都能得出正确结果 两种对于距离的计算方法 算法的时间复杂度均为$O(n)$ 动态规划 思考方式与证明 假设已经计算出来了$a[i]$之前所有的机器人两两之间距离之和为$s$ 增加一个元素$a[i]$,观察其和之前的所有机器人之间的距离之和,能够发现 实际上就是与之前的每个元素计算距离 $a[i]$与$a[0],a[1]….,a[i-1]$之间的距离实际上分别是是$a[i-1]$与$a[0]….,a[i-1]$之间的距离加上$a[i]$与$a[i-1]$之间的距离,共$i$个 上述结论可以推导: 设$distance[i]$表示$a[i]$和$a[0],a[1]….,a[i-1]$之间的距离之和,有 $$ distance[i]=i*a[i]+distance[i-1] \distance[0]=0 $$ 代码 可以看出来这个公式只与前一个状态有关,因此可以优化空间 class Solution: def sumDistance(self, nums: List[int], s: str, d: int) -> int: mod=1000000007 n=len(nums) for i,a in enumerate(s): nums[i]+=d if s[i]=='R' else -d nums.sort() res=0 distance=0 for i in range(1,n): distance+=i*(nums[i]-nums[i-1]) #这个方法对于只int的话4*(10**9)会超范围 distance%=mod res+=distance return res%mod 贡献距离法 思考方式 对于这种什么什么之和的问题可以考虑计算每个片段对结果的贡献 在这个题目之中片段就是每个元素之间的距离,我们需要计算的就是这段距离在结果中出现了多少次 为了方便理解将每次计算距离视为两点之间的连线,看片段上线的数目就知道这个片段的使用次数 从$a[0]-a[1]$这一的角度来看:从$a[0]$连出去了$n-1$条线,每条线都经过了$a[1]$,所以得出$a[0]-a[1]$段的贡献是$n-1$ 从$a[1]-a[2]$的角度来看,$a[1]$连出去的$n-2$也就是$(n-i-1)$条线覆盖了$a[1]-a[2]$ 但还不够,注意到$a[0]$发出去的线只有一条落在了$a[1]$上,剩下的线也覆盖了$a[2]$ 希望确切地知道此时到底有多少条线覆盖了这段距离 证明 设$f[i]$表示$a[i]$到$a[i+1]$这段距离对结果的贡献次数 同时可知$f[i]$也表示此时距离上面的线段数量 从a[i]发出的所有向右的线都会做贡献,这个次数为$n-i$ 发现$a[i]$之前的线段,也就是参与了$f[i-1]$计算的线段,有一部分落在了$a[i]$上,剩下的部分继续参与$f[i]$的计算 可知落在$a[i]$上的线段实际上是a[i]发出的指向左边的线段,其数量为$i$ 综上 $f[0]=n-i\f[i]=(f[i-1]-i+n)-i=f[i-1]-2*i+n$ $$\displaystyle res=\sum_{i=0}^{n-2}f[i]*(a[i+1]-a[i])$$ Code 可以看出只与前一个状态有关,进行空间优化 代码为了方便对于$f[i]$计算的是$a[i+1]$到$a[i]$ class Solution: def sumDistance(self, nums: List[int], s: str, d: int) -> int: mod=1000000007 n=len(nums) for i,a in enumerate(s): nums[i]+=d if s[i]=='R' else -d nums.sort() pre=0 res=0 for i in range(n-1): pre=(pre+n-2*i-1)%mod res=(res+(nums[i+1]-nums[i])*pre)%mod return res 前缀和的形式化证明 下面是灵神的证明方式,与上面的证明相比更加数学,如下 $ (a[i]−a[0])+(a[i]−a[1])+⋯+(a[i]−a[i−1])=i⋅a[i]−(a[0]+a[1]+⋯+a[i−1])\$ $(a[0]+a[1]+⋯+a[i−1])$可根据前缀和求 下面是灵神的代码

↗

题解-在二叉树中分配硬币-深度优先搜索,自底向上考虑

Problem: 979. 在二叉树中分配硬币 思路 考虑最后计算成本的时候实际上是计算的是每个节点对硬币的操作数 每个节点只需要一个硬币,如何确定多出来的或少的硬币的流向 考虑自底向上,叶子节点只需要一个硬币,这样就能向父节点要硬币或者向父节点送硬币,之后父节点变成了叶子节点(其子节点已完成使命) 设一个值为$Need$表示需要向上移或者向上要的硬币数,根据以上推论我们能够得到如下公式,正数表示需要向上移动,负数表示需要上方向下移动 $$Need_{节点}=Need_{子节点}+val-1$$ 每次将一个节点处理完就将Need的绝对值加入到操作数之中 Code class Solution: def distributeCoins(self, root: Optional[TreeNode]) -> int: self.res=0 def dfs(node:TreeNode): if node==None: return 0 left=dfs(node.left) right=dfs(node.right) self.res+=abs(left)+abs(right) return right+left+node.val-1 dfs(root) return self.res class Solution { public: int res=0; int dfs(TreeNode* root){ if (!root) return 0; int left=dfs(root->left); int right=dfs(root->right); res+=abs(left)+abs(right); return left+right+root->val-1; } int distributeCoins(TreeNode* root) { dfs(root); return res; } };

↗

题解-困于环中的机器人-模拟与相关证明

思路 对于机器人,如果走结束后方向仍在北方,且不在原点才会脱离圈,不然就会困在圈里 因此只需要模拟即可 证明 假设我们第一次位移结束之后之后机器人所在的坐标为(x,y) 我们按照此时机器人面向的方向讨论 此时面向北方的:下一次前进路线和上次一致,第二次前进之后是(2x,2y),以此类推因此只要x,y不全为0则不会回到原点 此时面向南方: 考虑转换参考系,以面朝的方向为y正半轴,此时两个参考系实际上是180度旋转的,在新参考系下前进了(x,y),并转换了方向,在之前的参考系下,下一次的前进方向与这一次相反,也就是说下一次会移动(-x,-y)的位移,并且下次移动之后会转向北方:与没有动之前一致,进入循环 面向东方:同理,不过是每次都是翻转了90度的方向则位移路径为 (0,0)->(x,y)->(x+y,y-x)->(x+y-x,y-x-y)->(x+y-x-y,y-x-y+x),又到0,且方向回到了北方 面向西方同理 也相当于一个向量乘同一个旋转矩阵和位移矩阵多次但是方向需要另外特判一下 实现思路 用一组数字记录当前坐标 方向只有四种,且每个方向前进的方式不同,所以我们可以用数组存储每一种前进方式,根据方向增加 用一个单独的数字记录当前面向的方向 按照操作,模拟前进方式 判断 代码 class Solution { public: bool isRobotBounded(string instructions) { vector<vector<int>> direction{{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; int x=0,y=0,now_direction=0; for (char choice:instructions){ switch(choice){ case 'G': x+=direction[now_direction][0]; y+=direction[now_direction][1]; break; case 'L': now_direction-=1; if (now_direction<0) now_direction+=4; break; case 'R': now_direction+=1; now_direction%=4; break; default: break; } } return !((x!=0||y!=0)&&now_direction==0); } };

↗

题解-移位字符串分组-哈希表

Problem: 249. 移位字符串分组 思路 类似于凯撒密码的归类方式,我们将所有能通过轮转得到的字符串放在一起,不难想到可以通过暴力来对每一个字符串匹配 对于字符串仔细观察可以发现每一个可以归类到一起的字符串的本质是相同的,不只是字符串长度,还有与左右字母之间的差值 综上,对于每一类字符串我们只需要将其转化为模板的样子就能判断是否能归类 我们的问题就是如何将字符转成一个标准的模板 考虑字符之间的相对关系, 我们设首位为0,后面的每一位对应的是字符串该位与前一位的差 如果小于0则加26,来保证模式字符串相等 解题方法 对于每一个字符串,计算其相对字符的模板字符串 查询哈希表,如果有则插入到相对的答案组中 没有就新开一个答案组 重复上述操作 复杂度 时间复杂度: 只遍历一次 $O(mn)$,m为字符串长度 空间复杂度: 运用了哈希表 $O(n)$ Code class Solution { public: vector<vector<string>> groupStrings(vector<string>& strings) { vector<vector<string>>res; unordered_map<string,int>hash;//模板字符串及其对应的在答案的行数 for (auto &iter:strings){ string tmp="0_"; char a=iter[0]; int now; for (int i=1;i<iter.size();i++){ now=iter[i]-a; if (now<0)//保证模板的一致性 now+=26; tmp+=to_string(now)+"_"; a=iter[i]; } if (hash.find(tmp)!=hash.end()){ res[hash[tmp]].emplace_back(iter); } else{ hash[tmp]=res.size(); res.emplace_back(vector<string>(1,iter)); } } return res; } };

↗

题解-全局倒置和部分倒置-找数学规律

思路 根据题意可知全局倒置包含局部倒置,对于本题如果有 非局部倒置 的全局倒置,则返回false,不然就返回true 很容易相出用暴力去搜索自己2位之后的数组元素中是否有比自己小的,时间复杂度:$O(n^2)$ 倒置是根据的是递增数组出现的,也就是说我们得到的数组实际上是对递增数组进行过几次置换的数组 根据定义可以知道局部倒置的实质就是将相邻的数组元素进行的交换,所以要做的就是与原数组进行比较 位置不变的跳过 位置变化的判断是否和原数组相邻位置的值相等,相等则跳过 不满足上述两个条件的就是全局置换,返回false 代码 class Solution { public: bool isIdealPermutation(vector<int>& nums) { int n=nums.size()-1; int i=0; while (i<n){ if (nums[i]==i) i++; else if (nums[i]==i+1&&nums[i+1]==i) i+=2; else return false; } return true; } };

↗