开篇
5.6那天参加了PDD的后端岗位笔试,明显感觉题不是很难,但是做起来力不从心,所以AC率感人。在这里就不丢人现眼了,一共有四道题,我都截了图,最近重新做了两道,在这里分享一下。
说明:这两道题都是leetcode的原题,大家也可以参加leetcode中的945和473,下面说一下我的思路。
题目一.使数组唯一的增量最小
我们的目的是通过对一些元素进行递增,使得数组中没有重复元素,题目很容易理解,这道题我们将要说三种做法。
1.数组排序贪心法
我们首先将数组进行排序,并设置一个先前元素值,这个先前元素将被用来和数组中后面的元素进行比较,如果不重复,后面的元素应该是先前元素+1;如果不是的话我们要进行相应的操作,例如后者>先前元素+1,那么我们就要将后者更新为prenums;如果后者<先前元素+1,那么后这就需要自增,先前元素也需要得到相应改变。
可能我们描述起来不是那么形象,但是代码很清晰明了
代码
#include <algorithm>
class Solution {
public:
int minIncrementForUnique(vector<int>& A) {
if(A.size() == 0)
return 0;
sort(A.begin(),A.end());
int prenum = A[0];
int res = 0;
for(int i = 1;i < A.size();i++)
{
if(A[i] == prenum + 1)
prenum = A[i];
else if(A[i] > prenum + 1)
{
prenum = A[i];
}
else
{
res += (prenum + 1 - A[i]);
prenum++;
}
}
return res;
}
};
这个方法的时间复杂度为O(nlogn),主要是排序的时间复杂度,因为我们只遍历了一次数组而已。
解法二.并查集
有没有不需要排序就可以解决问题的方法呢?答案当然是用的,就是一种以时间换取空间的方法。
说到以时间换空间,我们很自然地想到了采用某种数据结构,但是这个问题我个人实在是想不到什么好的数据结构了,于是在leetcode上借鉴了大佬liweiwei的解法(荣幸加了一波微信),使用并查集来解决这个问题, 这个做法确实很难想到。
在这里我们就只说思路,不做代码演示了。
并查集中的基本操作其实没多大的区别,但是要包含路径压缩,而且我们是将大数连接到小数上面。它的初始化操作有一些不同,我们在将一个新的数加入到并查集中,我们先看看它左边的数在不在,即比它少1的数,如果在,将二者相连;再看看右边的数在不在,即比它第一的数,如果在,将二者相连,即可将一个新的数字加入到并查集中。这个操作我们称为init。
加入数据的过程中,我们先判断数字是否在并查集中,如果在,说明已经重复了,我们就需要找到这个并查集的根节点,因为根节点是当前最大的结点,我们将这个数字变为根节点+1,并加入到并查集中(调用init函数),然后计算变化幅度,与加入到最终结果中。
代码略过,大家可以参考leetcode945
解法三.最小增量数组
我们遍历数组,将其中的元素作为索引,出现次数作为值加入到一个vector数组中,然后根据每个数出现的次数,求出一个增量数组,增量大小和元素的数目有关,因为元素不能重复。
代码
#include <vector>
class Solution {
public:
int minIncrementForUnique(vector<int>& A) {
vector<int>v,m(40005,0);
for(int e:A)
{
++m[e];
}
for(int i = 0;i < m.size();i++)
{
if(m[i] == 0)continue;
if(v.empty()||i > v.back())
{
for(int j = 0;j < m[i];j++)
v.emplace_back(i+j);
}
else if(i <= v.back())
{
int temp = v.back();
for(int j = 0;j < m[i];j++)
v.emplace_back(temp + 1 + j);
}
}
int res = 0;
for(int i = 0;i < A.size();i++)
//增量数组和原始数组的差即是我们的答案
//增量数组描述的是在原始数组基础上加一些数使得原始数组不重复的新数组
res += v[i] - A[i];
return res;
}
};
题目二.火柴拼正方形
我们用给出的火柴长度拼成一个正方形,如果能够拼成的话就输出true,否则false
算法设计
这里我们可以想到,如果这个数组中的所有长度加和不是4的倍数,那么一定不可以拼成一个正方形。而sum(nums)/4其实就是每条边的长度,这就变得很像0-1背包问题。我们需要为一条边分配火柴,火柴的分类方式多种多样,但只要可以分配出四条边即可。
所以我们首先想到了使用回溯法,回溯法需要一个约束函数,那么这里的约束函数就是每条边的长度一定相等,且为sum(nums)/4。
有了这个约束条件,我们就可以做选择,改变状态,回溯,撤销选择
,以下操作就是直接提到过的回溯法的标准模板。
由于我们只需要判断true或者false,所以递归搜索将作为条件判断,如果dfs(index+1)成立,那么dfs(index)也成立。
# DFS算法
class Solution(object):
def makesquare(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
if len(nums) < 4:
return False
sum_ = sum(nums)
if sum_ % 4 != 0:
return False
# 每条边边长
div = sum_ // 4
# 对边进行排序
nums.sort(reverse=True)
res = [0 for _ in range(4)]
# 深度优先搜索
def dfs(index):
if index == len(nums):
# 四条边都相等才能返回true
return res[0] == res[1] == res[2] == div
else:
for i in range(4):
# 如果这条边还有空余位置就可以加入
if res[i] + nums[index] <= div:
res[i] += nums[index]
# 递归操作作为条件判断,下面成立上面即成立
if dfs(index + 1):
return True
res[i] -= nums[index]
return False
return dfs(0)
算法很容易理解,就是标准的回溯法模板,可惜自己当时做的时候就没有想用回溯法,在动态规划法上磨磨蹭蹭,还有很多细节的纰漏。当然既然是类似0-1背包问题,那么动态规划法当然可以解决,这里不做介绍了,大家可以在leetcode上搜索473题的题解,即可看到动态规划法的解题思路。
总结
OK,面试的题目都是经过层层包装的生活场景,他不会直接告诉你它的考点,它的解题方法,而问题就难在我们不能及时看到这道题目的本质,就像这道火柴拼正方形,明显
的背包问题,用回溯法和动态规划法都可解。所以大家在刷题的同时一定要看到题目的本质,要记住类似的题目概述,在面对面试题的时候才能一针见血,把那些伪装扒得干干净净。自己在这方面能力还是差一些的,还要继续努力啦。