Leetcode刷题记录(尽量每日更新)
Leetcode877 石子游戏:
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
解题思路:
本能的感觉就是一道动态规划的题目,令dp[i][j]为石子堆的序号从i到j,当前的玩家与另一位玩家石头数目之差的最大值。那么状态转移方程式为 d p [ i ] [ j ] = { p i l e s [ i ] − d p [ i + 1 ] [ j ] if p i l e s [ i ] − d p [ i + 1 ] [ j ] > p i l e s [ j ] − d p [ i ] [ j − 1 ] p i l e s [ j ] − d p [ i ] [ j − 1 ] otherwise dp[i][j] = \begin{cases} piles[i]-dp[i+1][j] &\text{if } piles[i]-dp[i+1][j]>piles[j]-dp[i][j-1] \\ piles[j]-dp[i][j-1] &\text{otherwise } \end{cases} dp[i][j]={piles[i]−dp[i+1][j]piles[j]−dp[i][j−1]if piles[i]−dp[i+1][j]>piles[j]−dp[i][j−1]otherwise
其中,若i>j,则dp[i][j]=0,若i==j,则dp[i][i] = piles[i]
然后代码就很简单可以写出来了
解题代码(C++版本)
执行用时:8 ms, 在所有 C++ 提交中击败了50.14% 的用户
内存消耗:8.4 MB, 在所有 C++ 提交中击败了42.29% 的用户
class Solution {
public:bool stoneGame(vector<int>& piles) {int length = piles.size();int dp[length][length];for(int i=0;i<length;i++){dp[i][i] = piles[i];}for(int i=length-2;i>=0;i--){for(int j=i+1;j<length;j++){dp[i][j] = std::max(piles[i]-dp[i+1][j],piles[j]-dp[i][j-1]);}}return dp[0][length-1]>0;}
};
看题解之后的感悟
第一就是可以进行空间复杂度的优化。从状态转移方程式中可以看到,dp[i][j] 的值只和 dp[i+1][j]和dp[i][j-1]有关。即在计算 dp的第 i 行的值时,只需要使用到 dp 的第 i 行和第 i+1 行的值。
第二就是没想到能够直接用数学方法计算出第一个玩家肯定获胜。但在真实的应用场景中,存在解析解的题目并不多。
483 最小好进制
对于给定的整数 n, 如果n的k(k>=2)进制数的所有数位全为1,则称 k(k>=2)是 n 的一个好进制。
解题思路:
这道题需要用到一些数学的推理,首先非常简单通过求和公式得到:
n = 1 − k m + 1 1 − k ( 1 ) n = \frac{1-k^{m+1}}{1-k} (1) n=1−k1−km+1(1)
我们需要得到最小的k,因为n>3,数学直觉告诉我们(当然想要证明也很好证明,但是有的时候相信直觉就完事了),k越小m就要越大。n最大为 1 0 18 10^{18} 1018, 因 为 k > = 2 , k m < n < ( k + 1 ) m , m < log 1 0 18 , m < 60 因为k>=2,k^{m}
解决之后用一个二重循环,将m从大到小计算判断是否存在相应的k,然后返回相应的值,如果不存在,返回n-1即可
解题代码(C++版本)
class Solution {
public:string smallestGoodBase(string n) {long long nval = stol(n);int mMax = floor(log(nval)/log(2));for(int m = mMax;m>1;m--){int k = pow(nval,1.0/m);long mul = 1,sum = 1;for (int i = 0; i < m; i++) {mul *= k;sum += mul;}if(sum==nval){return to_string(k);}}return to_string(nval-1);}
};
剑指offer38 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
解题思路:
这题非常简单,C++中拥有next_permutation函数,此处本人首先由string构造一个vector,然后对该vector进行排序,之后再用该vector执行next_permutation函数,当函数运行结束时,返回的结果就是最终结果。
解题代码(C++版本)
class Solution {
public:vector<string> permutation(string s) {vector<char> per(s.begin(),s.end());sort(per.begin(),per.end());vector<string> ans;do{ans.emplace_back(per.begin(),per.end());}while(std::next_permutation(per.begin(),per.end()));return ans;}
};
剑指offer15 二进制中1的数目
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
解题思路:
首先介绍x&(x-1),这个算是能够让x的最后一个1归0,重复此操作直到n为0即可。
解题代码(C++版本)
class Solution {
public:int hammingWeight(uint32_t n) {int cnt = 0;while(n){n = n&(n-1);cnt++;}return cnt;}
};
Leetcode149 直线上最多的点数
给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
解题思路:
我们可以考虑枚举所有的点,假设直线经过该点时,该直线所能经过的最多的点数。
假设我们当前枚举到点 i,如果直线同时经过另外两个不同的点 j 和 k,那么可以发现点 i 和点 j 所连直线的斜率恰等于点 i 和点 k 所连直线的斜率。
于是我们可以统计其他所有点与点 i 所连直线的斜率,出现次数最多的斜率即为经过点数最多的直线的斜率,其经过的点数为该斜率出现的次数加一(点 i 自身也要被统计)。
如何统计斜率?
注意到,由于浮点数精度的问题不能准确地表示每条直线的斜率。斜率的表达式为: s l o p e = a b slope = \frac{a}{b} slope=ba来表示。将a和b同时除以最大公因数,就可以得到一个固定的二元组{ a g c d ( a , b ) , b g c d ( a , b ) \frac{a}{gcd(a,b)},\frac{b}{gcd(a,b)} gcd(a,b)a,gcd(a,b)b}表示该直线的斜率。
然后使用unordered_map实现计数。再加四个小优化:
1、在点的总数量小于等于 2 的情况下,我们总可以用一条直线将所有点串联,此时我们直接返回点的总数量即可;
2、当我们枚举到点 i 时,我们只需要考虑编号大于 i 的点到点 i 的斜率,因为如果直线同时经过编号小于点 i 的点 j,那么当我们枚举到 j 时就已经考虑过该直线了;
当我们找到一条直线经过了图中超过半数的点时,我们即可以确定该直线即为经过最多点的直线;
当我们枚举到点 i(假设编号从 0 开始)时,我们至多只能找到 n-i 个点共线。假设此前找到的共线的点的数量的最大值为 k,如果有 k ≥ n − i k≥n-i k≥n−i那么此时我们即可停止枚举,因为不可能再找到更大的答案了。
解题代码(C++版本)
class Solution{
private:int gcd(int a,int b){return b?gcd(b,a%b):a;}
public:int maxPoints(vector<vector<int>>& points) {int n = points.size();if(n<=2){return n;}int ret = 0;for(int i=0;i<n;i++){if (ret >= n - i || ret > n / 2) {break;}unordered_map<pair<int,int>,int> mp;for(int j=i+1;j<n;j++){int x = points[i][0]-points[j][0];int y = points[i][1]-points[j][1];if(x==0){y = 1;}else if(y==0){x = 1;}else{if(y<0){x = -x;y = -y;}int gcdXY = gcd(abs(x),abs(y));x /= gcdXY, y/=gcdXY;}mp[{x,y}]++;}int maxn = 0;for(auto &pr:mp){maxn = max(maxn,pr.second+1);}ret = max(ret,maxn);}return ret;}
};
Leetcode815 公交路线
给你一个数组 routes ,表示一系列公交线路,其中每个 routes[i] 表示一条公交线路,第 i 辆公交车将会在上面循环行驶。
例如,路线 routes[0] = [1, 5, 7] 表示第 0 辆公交车会一直按序列 1 -> 5 -> 7 -> 1 -> 5 -> 7 -> 1 -> … 这样的车站路线行驶。
现在从 source 车站出发(初始时不在公交车上),要前往 target 车站。 期间仅可乘坐公交车。
求出 最少乘坐的公交车数量 。如果不可能到达终点车站,返回 -1 。
解题思路:
因为需要解出的结果是最少乘坐的公交车的数量,因此在同一辆公交车上无论前往哪一个站点的消耗都是相同的,自然很容易想到对公交车模型建模,将每个公交车作为图中的一个节点,如果两辆公交车有同样的停靠站点,那么两个节点之间就有一条边。点和边模型建立完成之后用BFS算法解决问题。
那么用什么方式来进行建模,此处可以使用一个Hash表,每个站点对应一个公交车列表,通过该hash表就能很方便地确定有哪些节点之间存在边。图的数据结构本人采用了邻接表结构,构建完图之后使用BFS解决问题。
易出错的一些细节
- source和target相同时直接返回0,如果按照BFS会返回1
- 没有必要构建完整个Hash表之后再构建图结构,会变慢,可以两者同时进行
- C++不要使用at(),直接用下标运算符
解题代码(C++版本)
class Solution {
public:int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {if(source==target){return 0;}int n = routes.size();unordered_map<int,vector<int>> station_to_route;vector<set<int>> graph(n); //用邻接链表构图//构图代码,构建map和邻接链表for(int i=0;i<routes.size();i++){auto route = routes[i];for(int station:route){for(int j:station_to_route[station]){graph[i].insert(j);graph[j].insert(i);}station_to_route[station].push_back(i);}}
// for(const auto& vec:graph){
// cout<<"{";
// for(const auto v:vec){
// cout<
// }
// cout<<"}"<
// }vector<int> dis(n,-1);queue<int> q;for(int start:station_to_route[source]){dis[start] = 1;q.push(start);}while(!q.empty()){int cur = q.front(); q.pop();for(int neigh:graph[cur]){if(dis[neigh]==-1){dis[neigh] = dis[cur]+1;q.push(neigh);}}}int ans = INT_MAX;for(int des:station_to_route[target]){if(dis[des]!=-1){ans = min(ans,dis[des]);}}return ans==INT_MAX?-1:ans;}
};
Leetcode168 Excel表列名称
给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。
解题思路:
本质上就是一个进制转换的问题,只是需要注意的是此处不是从0开始而是从1开始。A = 1,Z = 26;
变为26进制的话就代表了Z = A0这样的写法。所以对于每一位,可以做减一处理。最后反转字符串即可。
解题代码(C++版本)
class Solution {
public:string convertToTitle(int columnNumber) {string ans;while(columnNumber>0){--columnNumber;ans+=(columnNumber)%26+'A';columnNumber/=26;}reverse(ans.begin(),ans.end());return ans;}
};
剑指offer37 序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树。
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
解题思路:
本质上是一个对二叉树的遍历问题,一般可以使用前序遍历方式来实现。例如,对提示样例中的例子,应该是1,2,None,None,3,4,None,None,5,None,None。
然后返回的时候用一些简单的字符串处理然后逆向加入即可
解题代码(C++版本)
class Codec {
private:void rserialize(TreeNode* cur, string& str){if(cur==nullptr){str+="None,";return;}str+=to_string(cur->val)+",";rserialize(cur->left,str);rserialize(cur->right,str);}TreeNode* rdeserialize(list<string>& dataArray){if (dataArray.front() == "None") {dataArray.erase(dataArray.begin());return nullptr;}TreeNode* root = new TreeNode(stoi(dataArray.front()));dataArray.erase(dataArray.begin());root->left = rdeserialize(dataArray);root->right = rdeserialize(dataArray);return root;}public:// Encodes a tree to a single string.string serialize(TreeNode* root) {string ans;rserialize(root,ans);return ans;}// Decodes your encoded data to tree.TreeNode* deserialize(string data) {list<string> dataArray;string str;for (auto& ch : data) {if (ch == ',') {dataArray.push_back(str);str.clear();} else {str.push_back(ch);}}if (!str.empty()) {dataArray.push_back(str);str.clear();}
// for(const auto& v:dataArray){
// cout<
// }return rdeserialize(dataArray);}
};
Leetcode1833 雪糕的最大数量
夏日炎炎,小男孩 Tony 想买一些雪糕消消暑。
商店中新到 n 支雪糕,用长度为 n 的数组 costs 表示雪糕的定价,其中 costs[i] 表示第 i 支雪糕的现金价格。Tony 一共有 coins 现金可以用于消费,他想要买尽可能多的雪糕。
给你价格数组 costs 和现金量 coins ,请你计算并返回 Tony 用 coins 现金能够买到的雪糕的 最大数量 。
注意:Tony 可以按任意顺序购买雪糕。
解题思路:
本质上就是个贪心问题。将costs数组排序之后按照从小到大的顺序依次购买直到没钱为止。
解题代码(C++版本)
class Solution {
public:int maxIceCream(vector<int>& costs, int coins) {std::sort(costs.begin(),costs.end());int ans = 0,i=0;while(i<costs.size()){if(coins<costs[i]){break;}ans++;coins-=costs[i++];}return ans;}
};
Leetcode1418 点菜展示表
给你一个数组 orders,表示客户在餐厅中完成的订单,确切地说, orders[i]=[customerNamei,tableNumberi,foodItemi] ,其中 customerNamei 是客户的姓名,tableNumberi 是客户所在餐桌的桌号,而 foodItemi 是客户点的餐品名称。
请你返回该餐厅的 点菜展示表 。在这张表中,表中第一行为标题,其第一列为餐桌桌号 “Table” ,后面每一列都是按字母顺序排列的餐品名称。接下来每一行中的项则表示每张餐桌订购的相应餐品数量,第一列应当填对应的桌号,后面依次填写下单的餐品数量。
注意:客户姓名不是点菜展示表的一部分。此外,表中的数据行应该按餐桌桌号升序排列。
解题思路:
这是一道模拟类型的题目。做模拟类型的题目首先就是把问题转化为计算模型。此题很显然是使用键值对map,命名为table_to_dish。该map有两层,外层是每个桌号点了哪些菜,内层是每道菜点了几例。由于桌号需要从小到大排序,因此外层使用普通map,内层可以使用hashmap。
题目给定的order变量,每一次order由人名、桌号和菜名组成。其中人名是无用信息,根据每次的桌号和菜名更新table_to_dish,最后再遍历table_to_dish即可生成最终的结果
解题代码(C++版本)
class Solution {
public:vector<vector<string>> displayTable(vector<vector<string>>& orders) {//unordered_map> dish_to_table; set<string> dish_set;map<int,unordered_map<string,int>> table_to_dish;vector<vector<string>> ans;for(const auto& vec:orders){string table = vec[1], dish = vec[2];int table_i = std::stoi(table);table_to_dish[table_i][dish]++;dish_set.insert(dish);}//测试
// for(const auto& s:dish_set){
// cout<
// }
// for(auto iter = table_to_dish.begin();iter!=table_to_dish.end();iter++){
// cout<first<<"->{";
// for(auto in_iter= iter->second.begin();in_iter!=iter->second.end();in_iter++){
// cout<first<<"->"<second<<", ";
// }
// cout<<"}"<
// }//cout<vector<string> first_lane;first_lane.push_back("Table");for(string s:dish_set){first_lane.push_back(s);}ans.push_back(first_lane);for(auto iter = table_to_dish.begin();iter!=table_to_dish.end();iter++){vector<string> cur;int table = iter->first;cur.push_back(to_string(table));auto m = iter->second;for(const auto& t:dish_set){cur.push_back(to_string(m[t]));}ans.push_back(cur);}return ans;}
};
Leetcode1711 大餐计数
大餐 是指 恰好包含两道不同餐品 的一餐,其美味程度之和等于 2 的幂。
你可以搭配任意两道餐品做一顿大餐。
给你一个整数数组 deliciousness ,其中 deliciousness[i] 是第 i 道餐品的美味程度,返回你可以用数组中的餐品做出的不同 大餐 的数量。结果需要对 1 0 9 + 7 10^9 + 7 109+7 取余。
注意,只要餐品下标不同,就可以认为是不同的餐品,即便它们的美味程度相同。
解题思路:
最朴素的做法是二重循环找到每个可能存在的对,其时间复杂度为 O ( n 2 ) O(n^2) O(n2),显然是超过了此题应该需求的复杂度。这题的关键是使用hash表。
上述朴素解法存在同一个元素被重复计算的情况,因此可以使用哈希表减少重复计算,降低时间复杂度。具体做法是,使用哈希表存储数组中的每个元素的出现次数,遍历到数组deliciousness 中的某个元素时,在哈希表中寻找与当前元素的和等于 2 的幂的元素个数,然后用当前元素更新哈希表。由于遍历数组时,哈希表中已有的元素的下标一定小于当前元素的下标,因此任意一对元素之和等于 2 的幂的元素都不会被重复计算。
令 maxVal 表示数组deliciousness 中的最大元素,则数组中的任意两个元素之和都不会超过 maxVal*2。令2maxSum=maxVal×2,则任意一顿大餐的美味程度之和为不超过 maxSum 的某个 2 的幂。
对于某个特定的 2 的幂sum,可以在 O(n)的时间内计算数组 deliciousness 中元素之和等于 sum 的元素对的数量。数组deliciousness 中的最大元素 maxVal 满足maxVal≤C,其中 C = 2 20 C=2^{20} C=220 ,则不超过 maxSum的2的幂有 O(log(maxSum))=O(maxVal})=O(log C) 个,因此可以在 O(nlogC) 的时间内计算数组 deliciousness 中的大餐数量。
解题代码(C++版本)
class Solution {
private:const int MOD = 1e9+7;
public:int countPairs(vector<int>& deliciousness) {//用hash表进行完成int maxVal = *max_element(deliciousness.begin(),deliciousness.end());int maxSum = maxVal+maxVal;int ans = 0;unordered_map<int,int> mp;int n = deliciousness.size();for(auto& val:deliciousness){for (int sum = 1; sum <= maxSum; sum <<= 1) {int cnt = mp.count(sum - val) ? mp[sum - val] : 0;ans = (ans + cnt) % MOD;}mp[val]++;}return ans;}
};
Leetcode930 和相同的二元组
给你一个二元数组 nums ,和一个整数 goal ,请你统计并返回有多少个和为 goal 的 非空 子数组。
子数组 是数组的一段连续部分。
解题思路:
依然是使用hash表进行完成,只不过此题需要加入前缀和技巧。
定义一个hash表unordered_map,template是
因为,连续段的子数组[i,j]的区间和可以用前缀和sum[j]-sum[i]表示,当每次遍历到一个节点时候,我们就知道有多少个前缀和为某个具体指(通过hash表),因此,找到sum-goal在hash表中对应的值就能知道对于特定的j,有多少个i的区间和为goal。
解题代码(C++版本)
class Solution {
public:int numSubarraysWithSum(vector<int>& nums, int goal) {int sum = 0;unordered_map<int, int> cnt;int ans = 0;for (auto& num : nums) {cnt[sum]++;sum += num;ans += cnt[sum - goal];}return ans;}
};
面试题17.10 主要元素
数组中占比超过一半的元素称之为主要元素。给你一个 整数 数组,找出其中的主要元素。若没有,返回 -1 。请设计时间复杂度为 O(N) 、空间复杂度为 O(1) 的解决方案。
解题思路:
先吐槽一下,这题居然是个简单题,但是我想了一个中午都没想出来这个解法。
如果是使用朴素的解法,使用hash表先遍历一遍数组,统计每个数字出现的次数,如果有数字出现的次数大于数组长度的 1 / 2 1/2 1/2,就直接返回该数字。但是此题需要设计时间复杂度为O(N),空间复杂度为O(1)的解法,显然使用hash表遍历数组的方式是无法完成该目标的。然后想了很久想不出来就去看题解了:题解使用的是Boyer-Moore投票算法。
Boyer-Moore 投票算法的基本思想是:在每一轮投票过程中,从数组中删除两个不同的元素,直到投票过程无法继续,此时数组为空或者数组中剩下的元素都相等。
如果数组为空,则数组中不存在主要元素;
如果数组中剩下的元素都相等,则数组中剩下的元素可能为主要元素。
Boyer-Moore 投票算法的步骤如下:
维护一个候选主要元素 candidate 和候选主要元素的出现次数 count,初始时 candidate 为任意值,count=0;
遍历数组nums 中的所有元素,遍历到元素 x 时,进行如下操作:
如果count=0,则将 x 的值赋给 candidate,否则不更新candidate 的值;
如果x=candidate,则将count 加 1,否则将count 减 1。
遍历结束之后,如果数组nums 中存在主要元素,则candidate 即为主要元素,否则candidate 可能为数组中的任意一个元素。
由于不一定存在主要元素,因此需要第二次遍历数组,验证candidate 是否为主要元素。第二次遍历时,统计 candidate 在数组中的出现次数,如果出现次数大于数组长度的一半,则candidate 是主要元素,返回 candidate,否则数组中不存在主要元素,返回−1。
为什么当数组中存在主要元素时,\text{Boyer-Moore}Boyer-Moore 投票算法可以确保得到主要元素?
在Boyer-Moore 投票算法中,遇到相同的数则将count 加 1,遇到不同的数则将count 减 1。根据主要元素的定义,主要元素的出现次数大于其他元素的出现次数之和,因此在遍历过程中,主要元素和其他元素两两抵消,最后一定剩下至少一个主要元素,此时candidate 为主要元素,且count≥1
解题代码(C++版本)
class Solution {
public:int majorityElement(vector<int>& nums) {int candidate = -1;int count = 0;for(int num:nums){if(count==0){candidate = num;}if(candidate==num){count++;}else{count--;}}count = 0;for(int num:nums){if(num==candidate){count++;}}return count*2>nums.size()?candidate:-1;}
};
Problem275 H指数II
给定一位研究者论文被引用次数的数组(被引用次数是非负整数),数组已经按照 升序排列 。编写一个方法,计算出研究者的 h 指数。
h 指数的定义: “h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (N 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。(其余的 N - h 篇论文每篇被引用次数不多于 h 次。)"
解题思路:
个人认为这题才是个简单题。注意到题目中h指数是共有h篇论文分别被引用了至少h次,然后其余的(N-h)篇论文每篇被引用次数不多于h次。由于数组已经按照升序排列了,因此时间复杂度最低的方式是使用二分法。
二分的判断条件如下:找到中心点mid,那么至少有n-mid篇文章引用度>=citations[mid],因此,如果citations[mid]>=n-mid,把right赋值为mid-1,否则把left赋值为mid+1。最后返回n-left就是需要的结果。
解题代码(C++版本)
class Solution {
public:int hIndex(vector<int>& citations) {int n = citations.size();int left = 0,right = n-1;while (left<=right){int mid = left+((right-left)/2);//cout<if(citations[mid]>=n-mid){right = mid-1;}else{left = mid+1;}}return n-left;}
};
Problem218 天际线问题
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:
lefti 是第 i 座建筑物左边缘的 x 坐标。
righti 是第 i 座建筑物右边缘的 x 坐标。
heighti 是第 i 座建筑物的高度。
天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],…] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]
解题思路:
困难题目一如既往地不会做。
观察题目我们可以发现,关键点的横坐标总是落在建筑的左右边缘上。这样我们可以只考虑每一座建筑的边缘作为横坐标,这样其对应的纵坐标为「包含该横坐标」的所有建筑的最大高度。
观察示例一可以发现,当关键点为某建筑的右边缘时,该建筑的高度对关键点的纵坐标是没有贡献的。例如图中横坐标为 7 的关键点,虽然它落在红色建筑的右边缘,但红色建筑对其并纵坐标并没有贡献。因此我们给出「包含该横坐标」的定义:建筑的左边缘小于等于该横坐标,右边缘大于该横坐标(也就是我们不考虑建筑的右边缘)。即对于包含横坐标 x 的建筑 i,有 x ∈ [ l e f t i , r i g h t i ] x ∈ [left_i,right_i] x∈[lefti,righti]
特别地,在部分情况下,「包含该横坐标」的建筑并不存在。例如当图中只有一座建筑时,该建筑的左右边缘均对应一个关键点,当横坐标为其右边缘时,这唯一的建筑对其纵坐标没有贡献。因此该横坐标对应的纵坐标的大小为 0。
这样我们可以想到一个暴力的算法:O(n)地枚举建筑的每一个边缘作为关键点的横坐标,过程中我们O(n) 地检查每一座建筑是否「包含该横坐标」,找到最大高度,即为该关键点的纵坐标。该算法的时间复杂度是 O ( n 2 ) O(n^2) O(n2),我们需要进行优化。
我们可以用优先队列来优化寻找最大高度的时间,在我们从左到右枚举横坐标的过程中,实时地更新该优先队列即可。这样无论何时,优先队列的队首元素即为最大高度。为了维护优先队列,我们需要使用「延迟删除」的技巧,即我们无需每次横坐标改变就立刻将优先队列中所有不符合条件的元素都删除,而只需要保证优先队列的队首元素「包含该横坐标」即可。
具体地,为了按顺序枚举横坐标,我们用数组boundaries 保存所有的边缘,排序后遍历该数组即可。过程中,我们首先将「包含该横坐标」的建筑加入到优先队列中,然后不断检查优先队列的队首元素是否「包含该横坐标」,如果不「包含该横坐标」,我们就将该队首元素弹出队列,直到队空或队首元素「包含该横坐标」即可。最后我们用变量maxn 记录最大高度(即纵坐标的值),当优先队列为空时,maxn=0,否则 maxn 即为队首元素。最后我们还需要再做一步检查:如果当前关键点的纵坐标大小与前一个关键点的纵坐标大小相同,则说明当前关键点无效,我们跳过该关键点即可。
在实际代码中,我们可以进行一个优化。因为每一座建筑的左边缘信息只被用作加入优先队列时的依据,当其加入优先队列后,我们只需要用到其高度信息(对最大高度有贡献)以及其右边缘信息(弹出优先队列的依据),因此只需要在优先队列中保存这两个元素即可。
解题代码(C++版本)
class Solution {
public:vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {auto cmp = [](const pair<int,int>& a,const pair<int,int>& b)->bool {return a.second<b.second;};priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> que(cmp);vector<int> boundaries;for(auto building:buildings){boundaries.push_back(building[0]);boundaries.push_back(building[1]);}sort(boundaries.begin(),boundaries.end());vector<vector<int>> ret;int n = buildings.size(), idx = 0;for (auto& boundary : boundaries) {while (idx < n && buildings[idx][0] <= boundary) {que.emplace(buildings[idx][1], buildings[idx][2]);idx++;}while (!que.empty() && que.top().first <= boundary) {que.pop();}int maxn = que.empty() ? 0 : que.top().second;if (ret.size() == 0 || maxn != ret.back()[1]) {ret.push_back({boundary, maxn});}}return ret;}
};
Problem1818 绝对差值和
给你两个正整数数组 nums1 和 nums2 ,数组的长度都是 n 。
数组 nums1 和 nums2 的 绝对差值和 定义为所有 |nums1[i] - nums2[i]|(0 <= i < n)的 总和(下标从 0 开始)。
你可以选用 nums1 中的 任意一个 元素来替换 nums1 中的 至多 一个元素,以 最小化 绝对差值和。
在替换数组 nums1 中最多一个元素 之后 ,返回最小绝对差值和。因为答案可能很大,所以需要对 1 0 9 + 7 10^9 + 7 109+7 取余后返回。
|x| 定义为:
如果 x >= 0 ,值为 x ,或者
如果 x <= 0 ,值为 -x
解题思路:
把这题要解决的数学表达式列出来:首先是所有的绝对值差之和 ∑ 0 ≤ i < n ∣ n u m s 1 [ i ] − n u m s 2 [ i ] ∣ \sum_{\mathclap{0\le i\lt n}}{|nums_1[i]-nums_2[i]|} 0≤i<n∑∣nums1[i]−nums2[i]∣
然后我们要修改这个式子,把其中某项 ∣ n u m s 1 [ i ] − n u m s 2 [ i ] ∣ |nums_1[i]-nums2[i]| ∣nums1[i]−nums2[i]∣替换成 ∣ n u m s 1 [ j ] − n u m s 2 [ i ] ∣ |nums_1[j]-nums_2[i]| ∣nums1[j]−nums2[i]∣,因此我们只需找到最大的 ∣ n u m s 1 [ i ] − n u m s 2 [ i ] ∣ − ∣ n u m s 1 [ j ] − n u m s 2 [ i ] ∣ |nums_1[i]-nums2[i]|-|nums_1[j]-nums_2[i]| ∣nums1[i]−nums2[i]∣−∣nums1[j]−nums2[i]∣就能找到结果。
对于特定的i,都找到相应的最大的j。如果使用朴素解法时间复杂度为O(n^2),但是如果使用二分法,先排序时间复杂度为O(nlogn),然后再查找时间复杂度为O(nlogn)
二分法需要把得到的mid结果的前后都查看一遍,否则有个算例会出现3177和3176的错误。
解题代码(C++版本)
执行用时:132 ms , 在所有 C++ 提交中击败了99.74%的用户
内存消耗:62.5 MB , 在所有 C++ 提交中击败了74.28%的用户
class Solution{
private:const int MOD = 1e9+7;int findMin(vector<int>& rec,int num1){//cout<int left = 0,right = rec.size()-1;while(left<right){int mid = (left+right)/2;//cout<if(rec[mid]>=num1){right = mid-1;}else{left = mid+1;}}if(left==0){return min(abs(rec[left]-num1),abs(rec[left+1]-num1));}else if(left==rec.size()-1){return min(abs(rec[left]-num1),abs(rec[left-1]-num1));}else{return min(abs(rec[left-1]-num1),min(abs(rec[left]-num1),abs(rec[left+1]-num1)));}}
public:int minAbsoluteSumDiff(vector<int>& nums1, vector<int>& nums2) {vector<int> rec(nums1);sort(rec.begin(),rec.end());//cout<int sum = 0,maxn = 0;int n = nums1.size();for(int i=0;i<n;i++){int diff = std::abs(nums1[i] - nums2[i]);sum = (sum + diff) % MOD;//找到nums1[j]与nums2[i]的差距最小//进行二分查找int j = findMin(rec,nums2[i]);maxn = max(maxn,diff-j);}return (sum-maxn+MOD)%MOD;}
};
Problem1846 减少和重新排列数组后的最大元素
给你一个正整数数组 arr 。请你对 arr 执行一些操作(也可以不进行任何操作),使得数组满足以下条件:
arr 中 第一个 元素必须为 1 。
任意相邻两个元素的差的绝对值 小于等于 1 ,也就是说,对于任意的 1 <= i < arr.length (数组下标从 0 开始),都满足 abs(arr[i] - arr[i - 1]) <= 1 。abs(x) 为 x 的绝对值。
你可以执行以下 2 种操作任意次:
减小 arr 中任意元素的值,使其变为一个 更小的正整数 。
重新排列 arr 中的元素,你可以以任意顺序重新排列。
请你返回执行以上操作后,在满足前文所述的条件下,arr 中可能的 最大值 。
解题思路:
提示 1
如果一个数组是满足要求的,那么将它的元素按照升序排序后得到的数组也是满足要求的。
提示 1 解释
假设数组中出现了元素 x 和 y,且 x
提示 2
在提示 1 的基础上,我们得到了一个单调递增的数组。那么数组中相邻两个元素,要么后者等于前者,要么后者等于前者加上 1。
我们可以先将数组进行升序排序,随后对数组进行遍历,将 arr[i] 更新为其自身与 arr[i−1]+1 中的较小值即可。
最终的答案(最大值)即为 arr 中的最后一个元素。
解题代码(C++版本)
执行用时:76 ms, 在所有 C++ 提交中击败了99.92%的用户
内存消耗:50.1 MB, 在所有 C++ 提交中击败了31.94%的用户
class Solution {
public:int maximumElementAfterDecrementingAndRearranging(vector<int>& arr) {int n = arr.size();sort(arr.begin(),arr.end());arr[0] = 1;for(int i=1;i<arr.size();i++){arr[i] = min(arr[i],arr[i-1]+1);}return arr.back();}
};
Problem1877 数组中最大数对和的最小值
一个数对 (a,b) 的 数对和 等于 a + b 。最大数对和 是一个数对数组中最大的 数对和 。
比方说,如果我们有数对 (1,5) ,(2,3) 和 (4,4),最大数对和 为 max(1+5, 2+3, 4+4) = max(6, 5, 8) = 8 。
给你一个长度为 偶数 n 的数组 nums ,请你将 nums 中的元素分成 n / 2 个数对,使得:
nums 中每个元素 恰好 在 一个 数对中,且
最大数对和 的值 最小 。
请你在最优数对划分的方案下,返回最小的 最大数对和 。
解题思路:
解题思路的数学证明乱七八糟的,还是让我来写一个:
最大数对和的最小值,方法就是排序+贪心:
将数组中所有的数进行排序,然后从两端开始向中间找数对的最大值,即 [ x 1 , x n ] , [ x 2 , x n − 1 ] . . . [x_1,x_n],[x_2,x_n-1]... [x1,xn],[x2,xn−1]...这样找到的最大值一定是最大数对和的最小值。证明方法如下:
2个数的情况平凡,假设4个数的情况为 x 1 , x 2 , x 3 , x 4 x_1,x_2,x_3,x_4 x1,x2,x3,x4,满足 x 1 ≤ x 2 ≤ x 3 ≤ x 4 x_1≤x_2≤x_3≤x_4 x1≤x2≤x3≤x4,那么一定是 ( x 1 , x 4 ) (x_1,x_4) (x1,x4)和 ( x 2 , x 3 ) (x_2,x_3) (x2,x3)这样的数对组合最大值最小,因为列举其他拆分方式,一定存在一个数对和大于此两个数对的和
对于n>4的情况,用反证法证明此结论。
假设用此方法找到的最大数对和为 [ x u , x v ] ( u < v 且 u + v = n + 1 ) [x_u,x_v](u
假设存在另外一种拆分方式,使得其最大数对和 [ x j , x k ] < [ x u , x v ] , 且 ( j + k ! = n + 1 ) [x_j,x_k]<[x_u,x_v],且(j+k!=n+1) [xj,xk]<[xu,xv],且(j+k!=n+1),否则就cover在之前的情况之中了。
反证:
1、假设 j + k > n + 1 j+k>n+1 j+k>n+1,那么 k > n + 1 − j k>n+1-j k>n+1−j,则 x k ≥ x n + 1 − j , 因 此 [ x j , x k ] ≥ [ x j , x n + 1 − j ] ≥ [ x u , x v ] x_k≥x_{n+1-j},因此[x_j,x_k]≥[x_j,x_{n+1-j}]≥[x_u,x_v] xk≥xn+1−j,因此[xj,xk]≥[xj,xn+1−j]≥[xu,xv]
2、假设 j + k < n + 1 j+k
两种情况都与假设情况不符,因此排序+贪心能够得到最优解。
解题代码(C++版本)
执行用时:228 ms, 在所有 C++ 提交中击败了96.94%的用户
内存消耗:94.1 MB, 在所有 C++ 提交中击败了18.85%的用户
class Solution {
public:int minPairSum(vector<int>& nums) {sort(nums.begin(),nums.end());
// for(int num:nums){
// cout<
// }cout<<endl;int left = 0,right = nums.size()-1;int maxnum = 0;while (left<right){maxnum = std::max(maxnum,nums[left]+nums[right]);//cout<left++; right--;}return maxnum;}
};
剑指offer52. 两个链表的第一个公共节点
因为该题有相关的图片,直接放出题目的超链接https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/
解题思路:
用hash表set存放链表A的每一个节点,然后遍历B的每一个节点查看是否在set中,然后返回第一个存在于set中的节点,如果都不存在,则返回null
解题代码(C++版本)
执行用时:48 ms, 在所有 C++ 提交中击败了75.89%的用户
内存消耗:16.6 MB, 在所有 C++ 提交中击败了7.01%的用户
class Solution {
public:ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {unordered_set<ListNode*> unorderedSet;ListNode *Ahead = headA;while(Ahead!= nullptr){unorderedSet.insert(Ahead);Ahead = Ahead->next;}ListNode *Bhead = headB;while(Bhead!= nullptr){if(unorderedSet.find(Bhead)!=unorderedSet.end()){return Bhead;}Bhead = Bhead->next;}return nullptr;}
};
Problem138 复制带随机指针的链表
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。 例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。 返回复制链表的头节点。 用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示: val:一个表示 Node.val 的整数。 random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。 你的代码 只 接受原链表的头节点 head 作为传入参数。解题思路:
如果只是普通的链表,可以依序逐个复制。但是次链表中带有一个随机指向的节点,导致可能在构建一个节点的过程中,其随机指向的节点还没有被建立,因此想到,如果用一个哈希表从旧的节点指向新的节点,然后进行递归创建就行了。
需要注意的是,创建完成之后就把新创建的节点加入hash表中,否则会出错。
解题代码(C++版本)
class Solution{
private:unordered_map<Node*,Node*> cachedNode;
public:Node* copyRandomList(Node* head) {if(head== nullptr){return nullptr;}if(cachedNode.find(head)==cachedNode.end()){Node* node = new Node(head->val);cachedNode[head] = node;node->next = copyRandomList(head->next);node->random = copyRandomList(head->random);}return cachedNode[head];}
};
Problem1893 检查是否区域内所有整数都被覆盖
给你一个二维整数数组 ranges 和两个整数 left 和 right 。每个 ranges[i] = [starti, endi] 表示一个从 starti 到 endi 的 闭区间 。 如果闭区间 [left, right] 内每个整数都被 ranges 中 至少一个 区间覆盖,那么请你返回 true ,否则返回 false 。 已知区间 ranges[i] = [starti, endi] ,如果整数 x 满足 starti <= x <= endi ,那么我们称整数x 被覆盖了。解题思路:
朴素解法,定义一个cnt[0,right-left+1],当有区间包含[left,right]的任何一个元素时,就令对应的cnt++。
题解中还有介绍用差分解法解决该问题的思路,但是我看不太懂,有兴趣的可以去自己查看一下。
解题代码(C++版本)
class Solution{
public:bool isCovered(vector<vector<int>>& ranges,int left,int right){vector<int> cnt(right-left+1,0); //cnt数组for(auto&& range:ranges){if(range[1]<left||range[0]>right) {continue;}else{int mi = std::max(left,range[0]), ma = std::min(right,range[1]);for(int j = mi-left;j<=ma-left;j++){cnt[j]++;}}}for(int i : cnt){if(i==0){return false;}}return true;}
};
Problem1713 得到子序列的最小操作次数
给你一个数组 target ,包含若干 互不相同 的整数,以及另一个整数数组 arr ,arr 可能 包含重复元素。 每一次操作中,你可以在 arr 的任意位置插入任一整数。比方说,如果 arr = [1,4,1,2] ,那么你可以在中间添加 3 得到 [1,4,3,1,2] 。你可以在数组最开始或最后面添加整数。 请你返回 最少 操作次数,使得 target 成为 arr 的一个子序列。 一个数组的 子序列 指的是删除原数组的某些元素(可能一个元素都不删除),同时不改变其余元素的相对顺序得到的数组。比方说,[2,7,4] 是 [4,2,3,7,2,1,4] 的子序列(加粗元素),但 [2,4,2] 不是子序列。解题思路:
反正我自己是不可能做得出这道题目的。
查看题解,解题的思路就是贪心+二分查找的方式
记数组target的长度为n,数组arr的长度为m。
根据题意,target 和 arr 这两个数组的公共子序列越长,需要添加的元素个数也就越少。因此最少添加的元素个数为 n 减去两数组的最长公共子序列的长度。
求两个数组最长的公共子序列是一个经典的问题,Leetcode题目为1143。在解题代码中我也给出了此问题的答案,Solution1143类
但是Solution1143类的时间复杂度为O(mn),此题如果用这种解法就会超时。此题多出了一个条件,就是target中所有元素互不相同。因此,使用一个unordered_map用于保存target中每个值向每个下标的映射,然后根据这个映射新建一个数组,然后对该数组找到最大上升子序列即可。
解题代码(C++版本)
class Solution1143{//最长公共子序列,经典的动态规划问题
public:int longestCommonSubsequence(string text1, string text2) {int n = text1.size(),m = text2.size();vector<vector<int>> dp(n+1,vector<int>(m+1,0));for(int i=1;i<=n;i++){char c1 = text1.at(i-1);for(int j=1;j<=m;j++){char c2 = text2.at(j-1);if(c1==c2){dp[i][j] = dp[i-1][j-1]+1;}else{dp[i][j] = std::max(dp[i-1][j],dp[i][j-1]);}}}return dp[n][m];}
};class Solution300{//求最长的递增子序列问题
public:int lengthOfLIS(vector<int>& nums) {if(nums.size()==0){return 0;}int n = nums.size();vector<int> dp(n);dp[0] = 1;int maxans = 1;for(int i=1;i<n;i++){dp[i] = 1;for(int j=0;j<i;j++){if(nums[j]<nums[i]){dp[i] = std::max(dp[i],dp[j]+1);}}maxans = std::max(maxans,dp[i]);}return maxans;}
};class Solution{
public:int minOperations(vector<int>& target, vector<int>& arr) {int n = target.size();unordered_map<int,int> pos;for(int i=0;i<n;i++){pos[target[i]] = i;}vector<int> d;for(int val:arr){if(pos.find(val)!=pos.end()){int idx = pos[val];auto it = lower_bound(d.begin(),d.end(),idx);if (it != d.end()) {*it = idx;} else {d.push_back(idx);}}}return n-d.size();}
};
Problem671 二叉树中第二小的节点
给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。 更正式地说,root.val = min(root.left.val, root.right.val) 总成立。 给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。解题思路:
这是一道简单题,因为root.val = min(root.left.val, root.right.val)总成立
所以第二小的值就是大于root.val的最小值
那么如何找到第二小的值呢,显然就是使用遍历,本人代码使用了深度优先遍历(当然广度优先遍历也是可以的)
解题代码(C++版本)
class Solution {
private:int rootVal;int ans;void dfs(TreeNode* node){if(node== nullptr){return;}if(ans!=-1&&node->val>=ans){return;}if(node->val>rootVal){ans = node->val;}dfs(node->left);dfs(node->right);}
public:int findSecondMinimumValue(TreeNode* root) {rootVal = root->val;ans = -1;dfs(root);return ans;}
};
Problem704 二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。解题思路:
一道非常简单而且经典的题目,二分查找问题。
给定的数列是排好序的,因此用二分查找的方式就能够以O(logn)的时间复杂度推算出数列中某个元素是否存在(若存在,其所处的位置)
解题代码(C++版本)
class Solution{
public:int search(vector<int>& nums,int target){int left = 0,right = nums.size()-1;while(left<=right){int mid = left+(right-left)/2;if(nums[mid]==target){return mid;}else if(nums[mid]<target){left = mid+1;}else{right = mid-1;}}return -1;}
};
Problem68 文本左右对齐
给定一个单词数组和一个长度 maxWidth,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。 你应该使用“贪心算法”来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格' '填充,使得每行恰好有 maxWidth 个字符。 要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。 文本的最后一行应为左对齐,且单词之间不插入额外的空格。 说明: 单词是指由非空格字符组成的字符序列。 每个单词的长度大于 0,小于等于 maxWidth。 输入单词数组 words 至少包含一个单词。解题思路:
一题比较考验代码能力的模拟题,根据题干描述的贪心算法,对于每一行,我们首先确定最多可以放置多少单词,这样可以得到该行的空格个数,从而确定该行单词之间的空格个数。
根据题目中填充空格的细节,我们分以下三种情况讨论:
1、当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格;
2、当前行不是最后一行,且只有一个单词:该单词左对齐,在行末填充空格;
3、当前行不是最后一行,且不只一个单词:设当前行单词数numWords,空格数为numSpaces,我们需要将空格均匀分配在单词之间,则单词之间应至少有 f l o o r ( n u m S p a c e s n u m W o r d s − 1 ) floor(\dfrac{numSpaces}{numWords-1}) floor(numWords−1numSpaces)个空格
对于多出来的空格 e x t r a S p a c e s = n u m S p a c e s % ( n u m W o r d s − 1 ) extraSpaces = numSpaces \% (numWords-1) extraSpaces=numSpaces%(numWords−1),则放到前面应填在前extraSpaces个单词之间。
解题代码(C++版本)
class Solution {
private://返回长度为n的空字符串string blank(int n){return string(n,' ');}string join(vector<string>& words,int left,int right,string sep){string ret = words[left];for(int i=left+1;i<right;i++){ret+=sep+words[i];}return ret;}
public:vector<string> fullJustify(vector<string>& words, int maxWidth) {int right = 0, n = words.size();vector<string> ans;while(true){int left = right;int sumLen = 0;while (right < n && sumLen + words[right].length() + right - left <= maxWidth) {sumLen += words[right++].length();}if(right==n){//当前行如果是最后一行string s = join(words,left,right," ");ans.emplace_back(s+blank(maxWidth-s.length()));return ans;}int numWords = right - left;int numSpaces = maxWidth - sumLen;if(numWords==1){ans.emplace_back(words[left]+blank(maxWidth-words[left].length()));continue;}int avgSpaces = numSpaces/(numWords-1);int extraSpaces = numSpaces%(numWords-1);string s1 = join(words, left, left + extraSpaces + 1, blank(avgSpaces + 1)); // 拼接额外加一个空格的单词string s2 = join(words, left + extraSpaces + 1, right, blank(avgSpaces)); // 拼接其余单词ans.emplace_back(s1 + blank(avgSpaces) + s2);}}
};
Problem600 不含连续1的非负整数
给定一个正整数 n,找出小于或等于 n 的非负整数中,其二进制表示不包含 连续的1 的个数。
示例1:
输入: 5
输出: 5
解释:
下面是带有相应二进制表示的非负整数<= 5:
0 : 0
1 : 1
2 : 10
3 : 11
4 : 100
5 : 101
其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。
解题思路:
按照惯例,我是做不出来这个困难的题目的…但是这一次认真把题解看懂了。简单来说这题是一个动态规划+字典树的问题
因为正整数 n 可以取到 1 0 9 10^9 109 ,所以显然是不可能通过暴力遍历从 1 到 n 的所有正整数来计算答案的。直观上,我们也可以感觉到,在暴力遍历的过程中,有非常多的计算是重复的。因此,我们考虑通过优化暴力遍历来解决这个问题。
为了形象地将重复计算的部分找出来,我们不妨将小于等于 n 的非负整数用 01 字典树的形式表示,其中的每一条从根结点到叶结点的路径都是一个小于等于 n 的非负整数(包含前导 0)。
于是,题目可以转化为:在由所有小于等于 n 的非负整数构成的 01 字典树中,找出不包含连续 1 的从根结点到叶结点的路径数量。

以 上图 n = 6 = ( 110 ) 2 n = 6 = (110)_2 n=6=(110)2为例,我们可以发现:
1、对于 01 字典树中的两个节点 n 1 n_1 n1和 n 2 n_2 n2,如果它们的高度相同,节点的值也相同,并且以它们为根结点的两棵子树都是满二叉树,那么它们包含的无连续 1 的从根结点到叶结点的路径个数是相同的。
2、对于 01 字典树中的两个结点 n 1 n_1 n1和 n 2 n_2 n2,如果 n 2 n_2 n2是 n 1 n_1 n1的子节点,并且它们的值都是 1,那么所有经过 n 1 n_1 n1和 n 2 n_2 n2的从根结点到叶结点的路径都一定包含连续的 1。
注意到由小于等于 n 的非负整数构成的01字典树是完全二叉树。于是有:如果某个结点包含两个子结点,那么其左子结点为根结点是 0 的满二叉树,其右子结点为根结点是 1 的完全二叉树;如果某个结点只有一个子结点,那么其左子结点为根结点是 0 的完全二叉树。
我们在计算不包含连续1的从根结点到叶结点的路径数量时,可以不断地将字典树拆分为根结点为 0 的满二叉树和根结点不定的完全二叉树。
于是,题目被拆分为以下两个子问题:
问题 1:如何计算根结点为 0 的满二叉树中,不包含连续 1 的从根结点到叶结点的路径数量。
问题 2:如何将将字典树拆分为根结点为 0 的满二叉树和根结点不定的完全二叉树。
算法
首先解决第 1 个问题。
我们发现,在高度为 t、根结点为 0 的满二叉树中:其左子结点是高度为 t−1、根结点为 0 的满二叉树。其右子结点是高度为 t−1、根结点为 1 的满二叉树;但是因为路径中不能有连续 1,所以右子结点下只有其左子结点包含的从根结点到叶结点的路径才符合要求,而其左子结点是高度为 t−2、根结点为 0 的满二叉树。
于是,高度为 t、根结点为 0 的满二叉树中不包含连续 1 的从根结点到叶结点的路径数量,等于高度为 t−1、根结点为 0 的满二叉树中的路径数量与高度为 t−2,根结点为 0 的满二叉树中的路径数量之和。因此,这个问题可以通过动态规划解决:
状态: d p [ t ] dp[t] dp[t]。 d p [ t ] dp[t] dp[t] 表示高度为 t−1、根结点为 0 的满二叉树中,不包含连续 1 的从根结点到叶结点的路径数量。
状态转移方程:
d p [ t ] = { d p [ t − 1 ] + d p [ t − 2 ] if t > = 2 1 if t < 2 dp[t] = \begin{cases} dp[t-1]+dp[t-2] &\text{if } t>=2 \\ 1 &\text{if } t<2 \end{cases} dp[t]={dp[t−1]+dp[t−2]1if t>=2if t<2
接着解决第 2 个问题。
考虑到 01 字典树作为完全二叉树所具有的性质,我们可以从根结点开始处理。如果当前结点包含两个子结点,则用问题 1 的解决方法计算其左子结点中不包含连续 1 的从根结点到叶结点的路径数量,并继续处理其右子结点;如果当前结点只包含一个左子结点,那么继续处理其左子结点。
在实现中,需要注意如果已经出现连续 1 则不用继续处理;另外,叶结点没有子结点,需要作为特殊情况单独处理。
解题代码(C++版本)
class Solution{
public:int findIntegers(int n){vector<int> dp(31);dp[0] = dp[1] = 1;for(int i=2;i<31;i++){dp[i] = dp[i-1]+dp[i-2];}int pre = 0,res = 0;for(int i=29;i>=0;i--){int val = 1<<i;if((n&val)!=0){res+=dp[i+1];if(pre==1){break;}pre = 1;}else{pre = 0;}if(i==0){res++;}}return res;}
};
Problem162 寻找峰值
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 n u m s [ − 1 ] = n u m s [ n ] = − ∞ nums[-1] = nums[n] = -∞ nums[−1]=nums[n]=−∞。
你必须实现时间复杂度为 O ( l o g n ) O(log n) O(logn) 的算法来解决此问题。
示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
示例2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
解题思路:
我采用的是O(n)的遍历方法,超过了100%的用户
代码非常简单,随便看看就行
解题代码(C++):
class Solution{
public:int findPeakElement(vector<int>& nums){if(nums.size()==1){return 0;}int i=0;while(i<nums.size()){if(i==0){if(nums[i]>nums[i+1]){return i;}}else if(i==nums.size()-1){if(nums[i]>nums[i-1]){return i;}}else{if(nums[i]>nums[i-1]&&nums[i]>nums[i+1]){return i;}}}i++;return 0;}
};
算法时间复杂度为O(logn)的随机算法
从一个点开始,不停地向高处走,最终一定能够达到一个峰值的位置。
对于当前可行的下标范围 [l,r],我们随机一个下标 i;
如果下标 i 是峰值,我们返回 ii 作为答案;
如果nums[i] 如果 nums[i]>nums[i+1],那么我们抛弃 [i, r] 的范围,在剩余 [l, i-1] 的范围内继续随机选取下标。 给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。 单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。 示例可以直接去看对应的题目 又是一道经典不会做的难题… 向前缀树中插入字符串 word; 查询前缀串 prefix 是否为已经插入到前缀树中的任意一个字符串 word 的前缀; 根据题意,我们需要逐个遍历二维网格中的每一个单元格;然后搜索从该单元格出发的所有路径,找到其中对应 \textit{words}words 中的单词的路径。因为这是一个回溯的过程,所以我们有如下算法: 遍历二维网格中的所有单元格。 深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。 如果当前路径是 words 中的单词,则将其添加到结果集中。如果当前路径是 words 中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是 words 中任意一个单词的前缀,则剪枝。我们可以将 words 中的所有字符串先添加到前缀树中,而后用 O(|S|) 的时间复杂度查询当前路径是否为 words 中任意一个单词的前缀。 在具体实现中,我们需要注意如下情况: 因为同一个单词可能在多个不同的路径中出现,所以我们需要使用哈希集合对结果集去重。 在回溯的过程中,我们不需要每一步都判断完整的当前路径是否是 words 中任意一个单词的前缀;而是可以记录下路径中每个单元格所对应的前缀树结点,每次只需要判断新增单元格的字母是否是上一个单元格对应前缀树结点的子结点即可。 请你判断一个 9x9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 注意: 一个有效的数独(部分已被填充)不一定是可解的。 示例可以直接去看对应的题目 一道模拟问题,并不涉及到什么复杂的算法 最初记事本上只有一个字符 ‘A’ 。你每次可以对这个记事本进行两种操作: Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。 示例1: 示例2: 那么如何找到代价最小值的式子呢?假设采用某种分解方式: 一条包含字母 A-Z 的消息通过以下的方式进行了编码: ‘A’ -> 1 “AAJF” 对应分组 (1 1 10 6) 除了 上面描述的数字字母映射方案,编码消息中可能包含 '’ 字符,可以表示从 ‘1’ 到 ‘9’ 的任一数字(不包括 ‘0’)。例如,编码字符串 "1" 可以表示 “11”、“12”、“13”、“14”、“15”、“16”、“17”、“18” 或 “19” 中的任意一条消息。对 “1*” 进行解码,相当于解码该字符串可以表示的任何编码消息。 给你一个字符串 s ,由数字和 ‘*’ 字符组成,返回 解码 该字符串的方法 数目 。 由于答案数目可能非常大,返回对 1 0 9 + 7 10^9 + 7 109+7 取余 的结果。 示例1: 示例2: 示例3: 一道比较典型的动态规划问题,这题的思考方式可以参考斐波那契数列,当前状态只和前一状态和前二状态有关,即 f [ i ] = A ∗ f [ i − 1 ] + B ∗ f [ i − 2 ] f[i] = A*f[i-1]+B*f[i-2] f[i]=A∗f[i−1]+B∗f[i−2] 给你 二维 平面上两个 由直线构成的 矩形,请你计算并返回两个矩形覆盖的总面积。 每个矩形由其 左下 顶点和 右上 顶点坐标表示: 第一个矩形由其左下顶点 (ax1, ay1) 和右上顶点 (ax2, ay2) 定义。 示例1: 示例2: 简单数学题:本质上就是找出两个矩形的重叠部分。 给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以 字符串形式返回小数 。 如果小数部分为循环小数,则将循环的部分括在括号内。 如果存在多个答案,只需返回 任意一个 。 对于所有给定的输入,保证 答案字符串的长度小于 1 0 4 10^4 104。 示例1: 示例2: 示例3: 示例4: 示例5: 比较复杂的数学题,首先第一点明确的就是用int在某些情况下会出现溢出( − 2 31 , − 1 -2^{31},-1 −231,−1}。因此需要定义long long类型。 给你一个由非负整数 a1, a2, …, an 组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。 实现 SummaryRanges 类: SummaryRanges() 使用一个空数据流初始化对象。 示例1: 首先最容易想到的方式是模拟,将 1 0 4 10^4 104个点用一个数组保存,当有这个点的时候为1,否则为0,然后当getInterval的时候全部遍历一遍。 在一个 1 0 6 ∗ 1 0 6 10^6*10^6 106∗106 的网格中,每个网格上方格的坐标为 (x, y) 。 现在从源方格 source = [ s x , s y ] [s_x, s_y] [sx,sy] 开始出发,意图赶往目标方格 target = [ t x , t y ] [t_x, t_y] [tx,ty] 。数组 blocked 是封锁的方格列表,其中每个 blocked[i] = [xi, yi] 表示坐标为 (xi, yi) 的方格是禁止通行的。 每次移动,都可以走到网格中在四个方向上相邻的方格,只要该方格 不 在给出的封锁列表 blocked 上。同时,不允许走出网格。 只有在可以通过一系列的移动从源方格 source 到达目标方格 target 时才返回 true。否则,返回 false。 示例1: 示例 2: 地图大小为 1 0 6 ∗ 1 0 6 10^6*10^6 106∗106,因此不能使用传统的BFS算法,因为 O ( 1 0 12 ) O(10^{12}) O(1012)的时间复杂度肯定超时。 考虑,起点到终点无法互相到达的情况。 起点,起点被block围住。 如果BFS过程中,找到终点,那么可以到达。 你正在参加一个多角色游戏,每个角色都有两个主要属性:攻击 和 防御 。给你一个二维整数数组 properties ,其中 properties[i] = [attacki, defensei] 表示游戏中第 i 个角色的属性。 如果存在一个其他角色的攻击和防御等级 都严格高于 该角色的攻击和防御等级,则认为该角色为 弱角色 。更正式地,如果认为角色 i 弱于 存在的另一个角色 j ,那么 attackj > attacki 且 defensej > defensei 。 返回 弱角色 的数量。 示例1: 示例 2: 示例 3: 单调栈问题。算法如下: n 张多米诺骨牌排成一行,将每张多米诺骨牌垂直竖立。在开始时,同时把一些多米诺骨牌向左或向右推。 每过一秒,倒向左边的多米诺骨牌会推动其左侧相邻的多米诺骨牌。同样地,倒向右边的多米诺骨牌也会推动竖立在其右侧的相邻多米诺骨牌。 如果一张垂直竖立的多米诺骨牌的两侧同时有多米诺骨牌倒下时,由于受力平衡, 该骨牌仍然保持不变。 就这个问题而言,我们会认为一张正在倒下的多米诺骨牌不会对其它正在倒下或已经倒下的多米诺骨牌施加额外的力。 给你一个字符串 dominoes 表示这一行多米诺骨牌的初始状态,其中: dominoes[i] = ‘L’,表示第 i 张多米诺骨牌被推向左侧, 示例1: 示例 2: 模拟题,有两种思路,第一种思路是从被施加过力的多米诺骨牌出发,第二种思路是从没有施加过力的骨牌出发。 首先是第一种,当时间为 0 时,部分骨牌会受到一个初始的向左或向右的力而翻倒。过了 1 秒后,这些翻倒的骨牌会对其周围的骨牌施加一个力。具体表现为: 向左翻倒的骨牌,如果它有直立的左边紧邻的骨牌,则会对该直立的骨牌施加一个向左的力。 接下去需要分析这些 1 秒时受力的骨牌的状态。如果仅受到单侧的力,它们会倒向单侧;如果受到两个力,则会保持平衡。再过 1 秒后,这些新翻倒的骨牌又会对其他直立的骨牌施加力,而不会对正在翻倒或已经翻倒的骨牌施加力。 这样的思路类似于广度优先搜索。我们用一个队列 que 模拟搜索的顺序;数组 time 记录骨牌翻倒或者确定不翻倒的时间,翻倒的骨牌不会对正在翻倒或者已经翻倒的骨牌施加力;数组force记录骨牌受到的力,骨牌仅在受到单侧的力时会翻倒。 第二种思路更为简单,从没有受到力的骨牌出发,找到他左右两侧受到力的骨牌,如果左侧受到向左的力,右侧也受到向左的力,那么在这两个骨牌中间的所有骨牌受到向左的力从而向左翻倒。同理,如果左侧右侧都向右那么所有骨牌也会受到向右的力而向右翻倒。如果左侧向左右侧向右那么中间所有骨牌维持不变。最后,如果左侧向右右侧向左,那么中间靠近左边的骨牌受到向右的力从而向右倾倒,中间靠近右侧的骨牌受到向左的力而向左倾倒,如果存在中心骨牌(与左右两侧距离相同),其维持不变。 共有 n 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序 从 1 到 n 编号。确切地说,从第 i 名小伙伴顺时针移动一位会到达第 (i+1) 名小伙伴的位置,其中 1 <= i < n ,从第 n 名小伙伴顺时针移动一位会回到第 1 名小伙伴的位置。 游戏遵循如下规则: 从第 1 名小伙伴所在位置 开始 。 示例1: 示例 2: 由于最近进了字节跳动青训营,所以尝试着多用Go写代码。 最直观的方法是模拟游戏过程。使用队列存储圈子中的小伙伴编号,初始时将 11 到 nn 的所有编号依次加入队列,队首元素即为第 11 名小伙伴的编号。 每一轮游戏中,从当前小伙伴开始数 kk 名小伙伴,数到的第 kk 名小伙伴离开圈子。模拟游戏过程的做法是,将队首元素取出并将该元素在队尾处重新加入队列,重复该操作 k - 1k−1 次,则在 k - 1k−1 次操作之后,队首元素即为这一轮中数到的第 kk 名小伙伴的编号,将队首元素取出,即为数到的第 kk 名小伙伴离开圈子。上述操作之后,新的队首元素即为下一轮游戏的起始小伙伴的编号。 每一轮游戏之后,圈子中减少一名小伙伴,队列中减少一个元素。重复上述过程,直到队列中只剩下 11 个元素,该元素即为获胜的小伙伴的编号。 以下用 f(n, k) 表示 n 名小伙伴做游戏,每一轮离开圈子的小伙伴的计数为 k 时的获胜者编号。 当 n = 1时,圈子中只有一名小伙伴,该小伙伴即为获胜者,因此 f(1, k) = 1。 当 n > 1时,将有一名小伙伴离开圈子,圈子中剩下 n - 1 名小伙伴。圈子中的第 k’名小伙伴离开圈子,k’满足 1≤k’≤n且k-k’应该是n 的倍数。由于 1≤k’≤n,因此 0 ≤ k’ - 1 ≤ n - 1。又由于k-k’是n的倍数等价于(k-1)-(k’-1)是n的倍数。可以得到k’ = (k-1) mod n+1 当圈子中剩下 n - 1 名小伙伴时,可以递归地计算 f(n - 1, k),得到剩下的 n - 1 名小伙伴中的获胜者。令 x = f(n - 1, k)。 由于在第 k’名小伙伴离开圈子之后,圈子中剩下的 n - 1 名小伙伴从第 k’ + 1 名开始计数。获胜者编号是从第 k’ + 1名小伙伴开始的第 x 名小伙伴,因此当圈子中有 n 名小伙伴时,获胜者编号是 将 x = f(n - 1, k)代入上述关系,可得:f(n, k) = (k + f(n - 1, k) - 1) mod n + 1 方法2的递归可以用迭代代替,减少栈空间的调用解题代码(C++):
class Solution2{
public:int findPeakElement(vector<int>& nums){int n = nums.size();// 辅助函数,输入下标 i,返回一个二元组 (0/1, nums[i])// 方便处理 nums[-1] 以及 nums[n] 的边界情况auto get = [&](int i) -> pair<int, int> {if (i == -1 || i == n) {return {0, 0};}return {1, nums[i]};};int left = 0, right = n - 1, ans = -1;while (left <= right) {int mid = (left + right) / 2;if (get(mid - 1) < get(mid) && get(mid) > get(mid + 1)) {ans = mid;break;}if (get(mid) < get(mid + 1)) {left = mid + 1;}else {right = mid - 1;}}return ans;}
};
Problem212 单词搜索II
解题思路:
前缀树(字典树)是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。前缀树可以用 O(|S|) 的时间复杂度完成如下操作,其中 |S| 是插入字符串或查询前缀的长度:解题代码(C++):
class Solution{
private:struct TrieNode{string word;unordered_map<char,TrieNode*> children;TrieNode(){this->word = "";}};void insertTrie(TrieNode* root,const string& word){TrieNode *node = root;for(auto c:word){if(!node->children.count(c)){node->children[c] = new TrieNode();}node = node->children[c];}node->word = word;}int dirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};public:bool dfs(vector<vector<char>>& board,int x,int y,TrieNode* root,set<string>& res){char ch = board[x][y];if (!root->children.count(ch)) {return false;}root = root->children[ch];if (root->word.size() > 0) {res.insert(root->word);}board[x][y] = '#';for (int i = 0; i < 4; ++i) {int nx = x + dirs[i][0];int ny = y + dirs[i][1];if (nx >= 0 && nx < board.size() && ny >= 0 && ny < board[0].size()) {if (board[nx][ny] != '#') {dfs(board, nx, ny, root,res);}}}board[x][y] = ch;return true;}vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {TrieNode * root = new TrieNode();set<string> res;vector<string> ans;for (auto & word: words){insertTrie(root,word);}for (int i = 0; i < board.size(); ++i) {for (int j = 0; j < board[0].size(); ++j) {dfs(board, i, j, root, res);}}for (auto & word: res) {ans.emplace_back(word);}return ans;}
};Problem36 有效的数独
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
只需要根据以上规则,验证已经填入的数字是否有效即可。解题思路:
就是对于每一个位置,查看是否在行、列、块中分别已经存在对应的数了,如果存在就返回false,否则继续,直到所有位置都检查完毕返回true
可以写几个辅助函数方便解决问题解题代码(C++):
class Solution {
private:bool check_line(vector<vector<bool>>& line,int i,int j,char c){if(c=='.'){return true;}else{int cur = c-'1';if(line[i][cur]){return false;}else{line[i][cur] = true;return true;}}}bool check_col(vector<vector<bool>>& col,int i,int j,char c){if(c=='.'){return true;}else{int cur = c-'1';if(col[j][cur]){return false;}else{col[j][cur] = true;return true;}}}bool check_block(vector<vector<bool>>& block,int i,int j,char c){if(c=='.'){return true;}else{int cur = c-'1';int cb = 3*(i/3)+(j/3);if(block[cb][cur]){return false;}else{block[cb][cur] = true;return true;}}}
public:bool isValidSudoku(vector<vector<char>>& board) {vector<vector<bool>> line(9,vector<bool>(9,false));vector<vector<bool>> col(9,vector<bool>(9,false));vector<vector<bool>> block(9,vector<bool>(9,false));for(int i=0;i<board.size();i++){for(int j=0;j<board[i].size();j++){if(!check_line(line,i,j,board[i][j])||!check_col(col,i,j,board[i][j])||!check_block(block,i,j,board[i][j])){return false;}}}return true;}
};
Problem650 只有两个键的键盘
Paste(粘贴):粘贴 上一次 复制的字符。
给你一个数字 n ,你需要使用最少的操作次数,在记事本上输出 恰好 n 个 ‘A’ 。返回能够打印出 n 个 ‘A’ 的最少操作次数。
输入:3
输出:3
解释:
最初, 只有一个字符 ‘A’。
第 1 步, 使用 Copy All 操作。
第 2 步, 使用 Paste 操作来获得 ‘AA’。
第 3 步, 使用 Paste 操作来获得 ‘AAA’。
输入:n = 1
输出:0
## 解题思路: 采用分解质因数的方法就可以解决。 首先,我们明白:对于任意特定的j,得到j个A的步骤为f(j),想要得到其整数倍k倍(k>1)的A的步骤为f(j*k) = f(j)+k步。注意:此式子中的k也是j*k数字的因数。此式子即为该问题的状态转移式,其意义如下: 给定初始值 n,我们希望通过若干次操作将其变为 1。每次操作我们可以选择 n 的一个大于 1 的因数 k,耗费 k 的代价并且将 n 减少为 n/j。在所有可行的操作序列中,总代价的最小值即为所要求的式子。
n = a 1 ∗ a 2 ∗ . . . ∗ a m n = a_1*a_2*...*a_m n=a1∗a2∗...∗am
对于任意的i∈[1,m](i为正整数),如果 a i a_i ai为质数,则其无法继续分解,如果 a i a_i ai为合数, a i = b i ∗ b j ( b i , b j > 1 ) a_i = b_i*b_j (b_i,b_j>1) ai=bi∗bj(bi,bj>1)
其拆分前的代价为 b i ∗ b j b_i*b_j bi∗bj,拆分后的代价为 b i + b j b_i+b_j bi+bj,那么一定有 b i ∗ b j > b i + b j b_i*b_j>b_i+b_j bi∗bj>bi+bj
因此,代价最小的式子就是分解成质因数解题代码(C++):
class Solution{
public:int minSteps(int n){int ans = 0;for(int i=2;i*i<=n;i++){while(n%i==0){n/=i;ans+=i;}}if(n>1){ans+=n;}return ans;}
};return false;}}}return true;}
};
Problem639 解码方法II
‘B’ -> 2
…
‘Z’ -> 26
要 解码 一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。例如,“11106” 可以映射为:
“KJF” 对应分组 (11 10 6)
注意,像 (1 11 06) 这样的分组是无效的,因为 “06” 不可以映射为 ‘F’ ,因为 “6” 与 “06” 不同。
输入:s = “"
输出:9
解释:这一条编码消息可以表示 “1”、“2”、“3”、“4”、“5”、“6”、“7”、“8” 或 “9” 中的任意一条。
可以分别解码成字符串 “A”、“B”、“C”、“D”、“E”、“F”、“G”、“H” 和 “I” 。
因此,"” 总共有 9 种解码方法。
输入:s = “1*”
输出:18
解释:这一条编码消息可以表示 “11”、“12”、“13”、“14”、“15”、“16”、“17”、“18” 或 “19” 中的任意一条。
每种消息都可以由 2 种方法解码(例如,“11” 可以解码成 “AA” 或 “K”)。
因此,“1*” 共有 9 * 2 = 18 种解码方法。
输入:s = “2*”
输出:15
解释:这一条编码消息可以表示 “21”、“22”、“23”、“24”、“25”、“26”、“27”、“28” 或 “29” 中的任意一条。
“21”、“22”、“23”、“24”、“25” 和 “26” 由 2 种解码方法,但 “27”、“28” 和 “29” 仅有 1 种解码方法。
因此,“2*” 共有 (6 * 2) + (3 * 1) = 12 + 3 = 15 种解码方法。
解题思路:
那么关键点就是如何求出A和B
不如自己写两个函数求A和B,本质上就是个分类讨论的问题解题代码(C++):
class Solution {
private:const int MOD = 1e9+7;int getA(char c){if(c=='*'){return 9;}else if(c=='0'){return 0;}else{return 1;}}int getB(char c1,char c2){if(c1>='3'&&c1<='9'||c1=='0'){return 0;}if(c1=='*'){if(c2=='*'){return 15;}else if(c2>='0'&&c2<='6'){return 2;}else{return 1;}}else if(c1=='1'){if(c2=='*'){return 9;}else{return 1;}}else if(c1=='2'){if(c2=='*'){return 6;}else if(c2>='0'&&c2<='6'){return 1;}else{return 0;}}return 0;}
public:int numDecodings(string s) {long long a = 1,b = getA(s[0]), c = b;for(int i=1;i<=s.size();i++){c = (getA(s[i])*b+getB(s[i-1],s[i])*a)%MOD;a = b;b = c;}return (int)c;}
};
Problem223 矩形面积
第二个矩形由其左下顶点 (bx1, by1) 和右上顶点 (bx2, by2) 定义。
输入:ax1 = -3, ay1 = 0, ax2 = 3, ay2 = 4, bx1 = 0, by1 = -1, bx2 = 9, by2 = 2
输出:45
输入:ax1 = -2, ay1 = -2, ax2 = 2, ay2 = 2, bx1 = -2, by1 = -2, bx2 = 2, by2 = 2
输出:16
解题思路:
那么首先判断是否有重叠部分,然后通过函数找出重叠部分减去重叠部分面积即可。解题代码(C++):
class Solution{
public:int computeArea(int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2) {//本题的关键就在于找到重合的部分//如果没有重合部分if(ax2<bx1||ax1>bx2||ay2<by1||ay1>by2){return (ax2-ax1)*(ay2-ay1)+(bx2-bx1)*(by2-by1);}int sum = (ax2-ax1)*(ay2-ay1)+(bx2-bx1)*(by2-by1);int cx1 = std::max(ax1,bx1),cy1 = std::max(ay1,by1),cx2 = std::min(ax2,bx2),cy2 = std::min(ay2,by2);return sum-(cx2-cx1)*(cy2-cy1);}
};
Problem166 分数到小数
输入:numerator = 1, denominator = 2
输出:“0.5”
输入:numerator = 2, denominator = 1
输出:“2”
输入:numerator = 2, denominator = 3
输出:“0.(6)”
输入:numerator = 4, denominator = 333
输出:“0.(012)”
输入:numerator = 1, denominator = 5
输出:“0.2”
解题思路:
然后首先处理整除的情况,简单判断一下直接返回即可
然后处理-号,处理完成之后将a与b都变为正数(abs()函数)
然后处理整数部分和’.’
最后处理小数部分,小数部分分为两种情况:能整除或者不能整除
对于无限循环的小数,只需要找到相同的余数部分就知道这一部分是循环体了,那么使用哈希表存储余数->index的映射解题代码(C++):
class Solution{
public:string fractionToDecimal(int numerator, int denominator) {auto a = (long long) numerator;auto b = (long long) denominator;//判断整除if(a%b==0){return to_string(a/b);}string s;if(a*b<0){s.append("-");a = abs(a);b = abs(b);}//整数部分auto c = abs(a/b);s.append(to_string(c));s.append(".");//小数部分string frac;unordered_map<long long,int> unorderedMap;long long r = a%b;int i = 0;while(r!=0&&!unorderedMap.count(r)){unorderedMap[r] = i;r*=10;frac.append(to_string(r/b));r%=b;i++;}if(r!=0){int insertIndex = unorderedMap.at(r);frac.insert(insertIndex,"(");frac.append(")");}s.append(frac);return s;}
};
Problem352 将数据流变成多个不相交区间
void addNum(int val) 向数据流中加入整数 val 。
int[][] getIntervals() 以不相交区间 [starti, endi] 的列表形式返回对数据流中整数的总结。
输入:
[“SummaryRanges”, “addNum”, “getIntervals”, “addNum”, “getIntervals”, “addNum”, “getIntervals”, “addNum”, “getIntervals”, “addNum”, “getIntervals”]
[[], [1], [], [3], [], [7], [], [2], [], [6], []]
输出:
[null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]]
解释:
SummaryRanges summaryRanges = new SummaryRanges();
summaryRanges.addNum(1); // arr = [1]
summaryRanges.getIntervals(); // 返回 [[1, 1]]
summaryRanges.addNum(3); // arr = [1, 3]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]]
summaryRanges.addNum(7); // arr = [1, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]]
summaryRanges.addNum(2); // arr = [1, 2, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]]
summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]解题思路:
显然这种计算的消耗太大。
然后想到用保存区间左端点到右端点的映射,可以用并查集的方式。但是每一次仍然需要遍历找左端点,本质上并没有加快多少速度。所以用set存储有序左端点。解题代码(C++):
class SummaryRanges {
private:vector<int> father; //存储右边界set<int> left;int find(int x){if(father[x]==x) return x;return father[x] = find(father[x]);}void Union(int x,int y){if(x>=0&&x<=10000&&father[x]!=-1&&father[y]!=-1){int fx = find(x),fy = find(y);if(fx!=fy){cout<<x<<" "<<y<<endl;cout<<fx<<" "<<fy<<endl;father[fx] = fy;left.erase(y);}}}
public:SummaryRanges() {father.resize(10002);for(int i=0;i<father.size();i++){father[i] = -1;}}void addNum(int val) {if(father[val]==-1){father[val] = val;left.insert(val);Union(val,val+1);Union(val-1,val);}}vector<vector<int>> getIntervals() {vector<vector<int>> ans;for(int start:left){ans.push_back({start, find(start)});}return ans;}
};
Problem1036 逃离大迷宫
输入:blocked = [[0,1],[1,0]], source = [0,0], target = [0,2]
输出:false
解释:
从源方格无法到达目标方格,因为我们无法在网格中移动。
无法向北或者向东移动是因为方格禁止通行。
无法向南或者向西移动是因为不能走出网格。
输入:blocked = [], source = [0,0], target = [999999,999999]
输出:true
解释:
因为没有方格被封锁,所以一定可以到达目标方格。解题思路:
一看,block的数量最大只有 200200,因此该题的突破口一定是这里。
终点,终点被block围住。
问题现在转化为 200 个 block,最多可以围住多少个格子?
将方块围成阶梯型,根据等差数列计算,最多可以围住n*(n-1)/2个格子。因此,可以分别从起点和终点BFS,判断是否被围住,最多遍历 (block)*(block-1)/2 个格子。
如果BFS过程中,没有找到终点,遍历了超过 (block)*(block-1)/2个格子,那么说明没有被围住,那么可以到达。
如果不是以上两种情况,即无法到达。
时间复杂度: O ( b l o c k 2 ) O(block^2) O(block2)
空间复杂度: O ( b l o c k 2 ) O(block^2) O(block2)解题代码(C++):
class Solution {
private:typedef long long ll;vector<int> source;vector<int> target;int MaxPoint;vector<vector<int>> dirs = {{-1,0},{1,0},{0,1},{0,-1}};unordered_set<ll> blockSet;bool isArrive(int x,int y,bool isSource){if(isSource&&x==target[0]&&y==target[1]) return true;if(!isSource&&x==source[0]&&y==source[1]) return true;return false;}//BFS求解bool bfs(int x,int y,bool is_source){unordered_set<ll> vis;queue<vector<int>> qu;vis.insert(x*(ll)1e6+y);qu.push({x,y});while(!qu.empty() &&(vis.size() <= MaxPoint)){auto cur = qu.front();qu.pop();int curx = cur[0],cury = cur[1];for(int i=0;i<4;i++){int nx = curx+dirs[i][0],ny = cury+dirs[i][1];if(nx<0||nx>=1e6||ny<0||ny>=1e6||blockSet.count(nx*(ll)1e6+ny)) continue;if(vis.count(nx*(ll)1e6+ny)) continue;if(isArrive(nx,ny,is_source)) return true;qu.push({nx,ny});vis.insert(nx*(ll)1e6+ny);}}return vis.size()>MaxPoint;}
public:bool isEscapePossible(vector<vector<int>>& blocked, vector<int>& source, vector<int>& target) {this->source = source;this->target = target;this->MaxPoint = ((blocked.size()+1)*(blocked.size()+2))/2;for(auto& vec:blocked){blockSet.insert(vec[0]*(ll)1e6+vec[1]);}return bfs(source[0], source[1], true) && bfs(target[0], target[1], false);}
};
Problem1996 游戏中弱角色的数量
输入:properties = [[5,5],[6,3],[3,6]]
输出:0
解释:不存在攻击和防御都严格高于其他角色的角色。
输入:properties = [[2,2],[3,3]]
输出:1
解释:第一个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。
输入:properties = [[1,5],[10,4],[4,3]]
输出:1
解释:第三个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。解题思路:
先把给定的数组排序,排序的策略是:攻击小的排在攻击力大的前面,相同攻击力的话防御力更高的排前面。然后从头到尾遍历,遍历过程维护一个栈st。遍历到某个角色p,如果p的防御力大于栈顶元素(那么当前角色p的攻击力一定大于栈顶元素对应的角色的攻击力),栈顶元素出栈,ans++;每个角色p都会入栈。
最后返回ans。解题代码(C++):
class Solution {
public:int numberOfWeakCharacters(vector<vector<int>>& properties) {sort(properties.begin(),properties.end(),[](const vector<int>& a,const vector<int>& b){return a[0]==b[0]?(a[1]>b[1]):(a[0]<b[0]); //按照攻击力从小到大排序,若有相同的攻击力,防御力更高的应该排前面});stack<int> defSt;int ans = 0;for(auto &p:properties){while(!defSt.empty()&&defSt.top()<p[1]){defSt.pop();ans++;}defSt.push(p[1]);}return ans;}
};
Problem838 推多米诺
dominoes[i] = ‘R’,表示第 i 张多米诺骨牌被推向右侧,
dominoes[i] = ‘.’,表示没有推动第 i 张多米诺骨牌。
返回表示最终状态的字符串。
输入:dominoes = “RR.L”
输出:“RR.L”
解释:第一张多米诺骨牌没有给第二张施加额外的力。
输入:dominoes = “.L.R…LR…L…”
输出:“LL.RR.LLRRLL…”解题思路:
向右翻倒的骨牌,如果它有直立的右边紧邻的骨牌,则会对该直立的骨牌施加一个向右的力。解题代码(C++):
class Solution {
public:string pushDominoes(string dominoes) {//广度优先搜索的模拟//只有当前仅受到一个方向的力时,才会向左侧或者右侧倾倒int n = dominoes.size();vector<int> time(n,-1);vector<string> force(n);queue<int> que;for(int i = 0;i<n;i++){char c = dominoes[i];if(c!='.'){que.push(i);time[i] = 0;force[i].push_back(dominoes[i]);}}string res(n,'.');while(!que.empty()){auto cur = que.front();que.pop();if(force[cur].size()==1){char f = force[cur][0];res[cur] = f;int next = (f == 'L') ? (cur - 1) : (cur + 1);if (next >= 0 and next < n) {int t = time[cur];if (time[next] == -1) {que.push(next);time[next] = t + 1;force[next].push_back(f);} else if(time[next] == t + 1) {force[next].push_back(f);}}}}return res;}
};class Solution1 {
public:string pushDominoes(string dominoes) {//另一种思路,从没有受到力的点出发,找到左侧和右侧的力int n = dominoes.size();for(int i=0;i<n;i++){if(dominoes[i]=='.'){int left = i;while(i<n&&dominoes[i]=='.'){i++;}int right = i;if(left==0||dominoes[left-1]=='L'){if(right<n&&dominoes[right]=='L'){for(int j=left;j<right;j++){dominoes[j] = 'L';}}}else{if(right==n||dominoes[right]=='R'){for(int j=left;j<right;j++){dominoes[j] = 'R';}}else{while(left<right-1){dominoes[left] = 'R';dominoes[right-1] = 'L';left++;right--;}}}}}return dominoes;}
};
Problem1823 找出游戏的获胜者
沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
否则,圈子中最后一名小伙伴赢得游戏。
给你参与游戏的小伙伴总数 n ,和一个整数 k ,返回游戏的获胜者。
输入:n = 5, k = 2
输出:3
解释:游戏运行步骤如下:1) 从小伙伴 1 开始。
2) 顺时针数 2 名小伙伴,也就是小伙伴 1 和 2 。
3) 小伙伴 2 离开圈子。下一次从小伙伴 3 开始。
4) 顺时针数 2 名小伙伴,也就是小伙伴 3 和 4 。
5) 小伙伴 4 离开圈子。下一次从小伙伴 5 开始。
6) 顺时针数 2 名小伙伴,也就是小伙伴 5 和 1 。
7) 小伙伴 1 离开圈子。下一次从小伙伴 3 开始。
8) 顺时针数 2 名小伙伴,也就是小伙伴 3 和 5 。
9) 小伙伴 5 离开圈子。只剩下小伙伴 3 。所以小伙伴 3 是游戏的获胜者。
输入:n = 6, k = 5
输出:1
解释:小伙伴离开圈子的顺序:5、4、6、2、3 。小伙伴 1 是游戏的获胜者。解题思路:
方法1:模拟+队列:
方法2:数学+递归:
f(n, k) = (k’ mod n + x - 1) mod n + 1 = (k + x - 1) mod n + 1解题代码(Go):
//方法1:直接模拟
func findTheWinner_method1(n int, k int) int {//fmt.Println(n)queue := make([]int, n)for i := range queue {queue[i] = i + 1}for len(queue) > 1 {for i := 1; i < k; i++ {queue = append(queue, queue[0])[1:]}queue = queue[1:]}return queue[0]
}//方法2:数学+递归
func findTheWinner_method2(n int, k int) int {if n == 1 {return 1}return (k+findTheWinner_method2(n-1, k)-1)%n + 1
}//方法3:数学+迭代
func findTheWinner_method3(n int, k int) int {if n == 1 {return 1}winner := 1for i := 2; i <= n; i++ {winner = (k+winner-1)%i + 1}return winner
}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
