字节跳动-2020秋招-笔试题剖析【5道算法题】,限时120分钟。
让我们一起来看看这些题吧!
题一:模型文件去重
【题目描述】
抖音上不同的用户类型我们有不同的用户模型文件。
我们有一个模型配置文件,里面有很多的不同的用户类型和他们对应的模型文件。我们需要找出每个模型对应的是哪些用户类型。
给定一行输入,格式是a b
a表示这个用户的用户类型,b表示这个用户对应的模型文件。
请你输出每个模型文件对应的用户类型。
注意1:每个模型文件可能对应多个用户类型,用户类型之间用空格作为切分。
注意2: 如果有多个用户类型输出,用户类型之间的排序按照字母表排序。
注意3: 如果有多个模型输出,模型输出的顺序按照模型文件在输入数据中顺序,即从上到下。
【输入描述】
输入第1行: 用户类型 N(表示有多少个 用户类型)
接下来的N行:用户类型 模型文件
【输出描述】
模型文件用户类型1 用户类型2
【示例】
输入:
1
abc 1.txt
输出:
1.txt abc
【解决思路及要点】
- 一个模型对应多个用户,用户需要按照字典序进行排序。针对这一点,可以把用户数据存入Set集合,并用哈希表把模型和用户的Set集合进行关联。
- 有多个模型的话模型输出的顺序按照模型文件在输入数据中顺序,可以把模型再存入List集合。
- 注意一下输出的格式。
【解决代码】
- 比较简单,没有注释。
public class Solution1 {
public static void main(String[] args) {
List<String> model=new ArrayList<>();
Map<String,Set<String>> table=new HashMap<>();
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
String a=new String();
String b=new String();
while(n-->0){
a=sc.next();
b=sc.next();
if(!table.containsKey(b)){
model.add(b);
table.put(b, new HashSet<String>());
}
table.get(b).add(a);
}
for(String m:model){
System.out.print(m);
for(String consumer:table.get(b)){
System.out.print(" "+consumer);
}
System.out.print("\n");
}
}
}
【题目剖析】
- 这个题不难,只需要注意一下用户的排序,模型的顺序,以及输出的格式就可以了。
- 这个题应该是可以在短时间内AC的。
题二:穿越沙漠的补给次数
【题目描述】
旅行者穿越沙漠的过程中需要不断地消耗携带的饮用水,到达终点前会经过几个绿洲,每个绿洲均设有水分补给站可以为旅行者提供水分补给并收取一定的费用。
沿途共有n个补给站,每个补给站收取的费用都一样,但是提供的水量不尽相同。起点到终点的距离为D公里,postion[i]表示第i个补给站距离起点的距离,单位为公里,supply[i]表示第i 个补给站可以提供的水量,单位为升。
假设旅行者在起点时携带了W升的水,每行走1公里需要消耗1升的水量,身上可携带的水量没有上限,且携带的水量多少不会对体能消耗产生影响,鉴于每次进补给站花费的钱都是一样多,期望用最少的补给次数到达终点,请帮忙计算最少的补给次数。
【输入描述】
第一行输入整数D和W, D表示起点到终点的距离,W表示初始携带的水量
第二行输入数组postion,长度为N,分别表示N个补给站分别距离起点的距离
第三行输入数组supply,长度为N, 分别表示N个补给站分别可以供给的水量
数据范围:1 <= D, W<=10^8, 0<=N<=1000, 0<position[i],supply[i]<D
【输出描述】
输出一个整数表示最少的补给次数,若无法到达终点则返回-1
【示例】
输入:
10 4
1 4 7
6 3 5
输出:
1
说明:
每行输入用空格隔开。起点到终点共10公里,初始时携带4升水,途径3个补给站。共需补给一次:只需在第1个补给站补给一次获得6升水,即可走完全程。
【解决思路及要点】
- 贪心的思想。
- 先假设用完身上所有的水,能走到一个位置,然后再去考虑去这个位置之前的水站加水,为了保证最少的加水次数,应该到能加到最多水的水站加水。
- 加完水后,需要标记,防止以后重复从这取水。
【解决代码】
public class Solution2 {
public static int minCount(int D,int W,int[] position,int[] supply){
int ans=0;
int n=position.length;
//标记数组,true代表已经取过水了
boolean[] flag=new boolean[n];
//当前所在位置
int currPos=0;
while(currPos<D){
//每次都把水喝完
currPos+=W;
W=0;
//如果走完,直接返回
if(currPos>=D){
return ans;
}
//记录能获得最多水的下标
int maxWater=-1;
for(int i=0;i<n;i++){
//还未达到相应的补给站
if(position[i]>currPos){
break;
}
//如果如果还没从该水站取水,就看在这里取水能否得到最大的水量
if(!flag[i]&&supply[i]>W){
W=supply[i];
maxWater=i;
}
}
//此时无水,并且没有可以取水的水站,无法到达
if(maxWater==-1){
return -1;
}
flag[maxWater]=true;
ans++;
}
return ans;
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int D=sc.nextInt();
int W=sc.nextInt();
sc.nextLine();
String s = sc.nextLine();
String[] s1 = s.split(" ");
int n=s1.length;
int[] position=new int[n];
int[] supply=new int[n];
for (int i = 0; i < n; i++) {
position[i]=Integer.valueOf(s1[i]);
}
s = sc.nextLine();
String[] s2 = s.split(" ");
for (int i = 0; i < n; i++) {
supply[i]=Integer.valueOf(s2[i]);
}
System.out.println(minCount(D,W,position,supply));
}
}
【题目剖析】
- 初看这个题感觉这个应该是dp的题,事实上确实也能用dp比较快速的解决,不过需要有大量dp的经验,能够立马提取出这个dp的模型,不然总是会在状态转移方程里迷惑。
- 贪心的思想就比较直接,要使用最少的补给,肯定是前往补给多的补给站加水。
- 事实上有贪心思想后,其实是比较疑惑的,这样做是不是一定正确呢?我感觉贪心思路要么是以前遇到过类似的,要么就要大胆的尝试,用理论证明贪心短时间内还是比较困难。
- 个人使用贪心的感觉是:先是用题目的数据,自己在大脑里去尝试用一下这种思想,发现没什么漏洞,和dp的思路也比较相近,就果断的使用了。
- 这道题可能不是很难,但是如果不熟悉dp或者贪心的话,可能无法在短时间内做出,也算是一个有区分度的题目了吧。
题三:走迷宫
【题目描述】
给定一个迷宫,找到最快从起点到达重点的路径所需要的步数。
假设迷宫如下,假定左上角坐标为(0,0),右下角坐标为 (3,2)
1 0 -1 1
-2 0 -1 -3
2 2 0 0
-2是迷宫的起点,坐标为(0,1)
-3是迷宫的终点,坐标为(3,1)
-1代表障碍物,不能行走
1和2代表传送门,传送门由正整数标示,只会成对出现。站在传送门上,能仅用一步就传送到相同数字的另一个传送门的位置:1只能传送到1,2只能传送到2。站在传送门上也可以选择不传送。
从起点到终点有若干种走法,举例如下:
(0,1)->(1,1)->(1,2)->(2,2)->(3,2)->(3,1),共花费5步
或者
(0,1)->(0,0)-传送>(3,0)->(3,1),共花费3步
经检验3步是所需的最少步数,最后结果返回3
【输入描述】
每一行输入都是用空格隔开的整数
第一行给出迷宫地图的长和宽,均为正整数
之后每一行的每一个数字,都代表迷宫的一格
-2表示起点,-3表示终点,-1表示不可通过的障碍物,0表示可通过的道路,大于0的正整数代表传送门,并且保证成对出现,在传送门上,可以仅用一步传送到另一个相同数字的传送门的位置。
迷宫大小<=200*200
【输出描述】
输出最少要多少步能够从起点走到终点。
输出-1如果没有任何办法从起点走到终点。
【示例】
输入:
4 3
1 0 -1 1
-2 0 -1 -3
2 2 0 0
输出:
3
说明:
(0,1)->(0,0)-传送>(3,0)->(3,1) ,共花费3步
【解决思路及要点】
- 经典的走迷宫问题,只不过加入了传送门,处理的思路也差不多。
- 采用BFS。
- 题目对迷宫的信息有些大,所以我们需要对迷宫里的点,当前的状态,传送门进行封装,达到减少代码量,并且方便理清思路的目的。
- 具体做法在代码中有完整的注释。
【解决代码】
public class Solution3 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
//获取长宽
String[] dimension = sc.nextLine().split(" ");
int width = Integer.valueOf(dimension[0]);
int height = Integer.valueOf(dimension[1]);
//迷宫
Square[][] map = new Square[width][height];
//一个y对应底图的一列
int y = 0;
//起始点和终点
Square start = null;
Square end = null;
//传送门对应的哈希表
Map<Integer, WarpGate> warpGate = new HashMap<>();
//获取地图,同时初始化相关的类
while (y<height) {
String line = sc.nextLine();
String[] squares = line.split(" ");
for (int x = 0; x < width; x++) {
map[x][y] = new Square(x, y, Integer.valueOf(squares[x]));
if (map[x][y].type == Square.SQUARE_START) {
start = map[x][y];
} else if (map[x][y].type == Square.SQUARE_END) {
end = map[x][y];
} else if (map[x][y].isWarpGate()) {
//建立传送门
int gateNumber = map[x][y].type;
WarpGate wg = warpGate.get(gateNumber);
if (wg == null) {
wg = new WarpGate();
warpGate.put(gateNumber, wg);
}
wg.buildWarpGate(map[x][y]);
}
}
y++;
}
if (start == null || end == null) {
throw new RuntimeException("未发现起始点或者终点!");
}
//迷宫创建成功,开始运算
System.out.println(solve(start, end, map, warpGate));
}
//走迷宫核心代码
public static int solve(Square start, Square end, Square[][] map, Map<Integer, WarpGate> warpGate) {
//标志是否访问过
Set<State> visited = new HashSet<>();
//BFS队列
Set<State> queue = new LinkedHashSet<>();
//放入起始点
queue.add(new State(start, 0));
while(!queue.isEmpty()) {
Iterator<State> currentIt = queue.iterator();
//获取当前当前走的点
State current = currentIt.next();
currentIt.remove();
//走到终点
if (current.square.equals(end)) {
return current.step;
}
//更新标志
visited.add(current);
//寻找下一个要走的点
current.nextStates(map, queue, visited, warpGate);
}
return -1;
}
//迷宫中的一个点
public static class Square {
public final int x;
public final int y;
public final int type;//这个点的类型
//题目中可能的类型
public static final int SQUARE_START = -2;
public static final int SQUARE_END = -3;
public static final int SQUARE_PATH = 0;
public static final int SQUARE_BLOCK = -1;
public Square(int x,int y,int type){
this.x = x;
this.y = y;
this.type = type;
}
//提供专门判断是否是传送门的方法
public boolean isWarpGate() {
return type > 0;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
//传送门类
public static class WarpGate{
public Square s1;
public Square s2;
//获取传送到的点,不是s1或者s2就返回null
public Square getOtherSide(Square s) {
if (s.equals(s1)) {
return s2;
} else if (s.equals(s2)) {
return s1;
} else {
return null;
}
}
//建立一个传送门
public void buildWarpGate(Square s) {
if (s1 == null) {
s1 = s;
} else if (s2 == null) {
s2 = s;
} else {
throw new RuntimeException("建立传送门异常!");
}
}
}
//一个点的状态
public static class State{
public final Square square;
public int step;
public State(Square square, int step) {
this.square = square;
this.step = step;
}
//更新到下一个未访问过的点
public void nextStates(Square[][] map, Set<State> toVisit,
Set<State> visited, Map<Integer, WarpGate> warpGate){
//左边的点存在,尝试加入队列
if (square.x > 0) {
Square left = map[square.x - 1][square.y];
addUnvisitedStateToList(left, toVisit, visited);
}
//右边的点存在,尝试加入队列
if (square.x < map.length - 1) {
Square right = map[square.x + 1][square.y];
addUnvisitedStateToList(right, toVisit, visited);
}
//下边的点存在,尝试加入队列
if (square.y > 0) {
Square up = map[square.x][square.y - 1];
addUnvisitedStateToList(up, toVisit, visited);
}
//上边的点存在,尝试加入队列
if (square.y < map[0].length - 1) {
Square down = map[square.x][square.y + 1];
addUnvisitedStateToList(down, toVisit, visited);
}
//尝试走传送门
WarpGate gate = warpGate.get(square.type);
if (gate != null) {
addUnvisitedStateToList(gate.getOtherSide(square), toVisit, visited);
}
}
private void addUnvisitedStateToList(Square s, Set<State> toVisit, Set<State> visited) {
State resultState = new State(s, this.step + 1);
//满足条件的点加入队列
if (s.type != Square.SQUARE_BLOCK && !visited.contains(resultState) && !toVisit.contains(resultState)) {
toVisit.add(resultState);
}
}
}
}
【题目剖析】
- 这个题应该是这场比试区分度最大的一个题了,因为真的很考验编码能力。
- 采用BFS的思路大家应该一般都很熟悉,主要是如何把一些细节考虑完善,如何封装里面的一些信息。
- 稍有不慎,出了点错误,再想改正就比较麻烦了。
- 在这种迷宫问题里,debug的时间消耗是非常大的,所以这就这就非常考验平时的编码能力,能一遍过,就已经很优秀了。
- 如果第一遍没有过,那么可以先做后面的题,这个题虽然说题型比较常见,但也不是那么容易AC的。
题四:简单变换
【题目描述】
春节在家的凯凯真的是太无聊了,他准备和他家的猫玩一个游戏。
凯凯在黑板上写下了两个长度相等的数列a[1…n], b[1…n]。
现在他想让他的猫判断数列a能否通过一个操作变换成数列b。
这个操作是:在数列a中选择一个区间l-r,对这个区间所有的数字加上一个k。
其中1<=l<=r<=n, k>=0。
你可以帮帮可怜的小猫做出这个判断么?
【输入描述】
首先输入一个数字t,表示有t组数据
每组数据的第一行为一个数字n 表示数列的长度
接下来两行每行有n个数字,分别为数组a和数组b
t<=10
n<=100000
【输出描述】
对于每组数据输出YES 或者 NO
表示数列a能否通过对应的操作变换成数列b。
【示例】
输入:
2
6
3 7 1 4 1 2
3 7 3 6 3 2
5
1 1 1 1 1
1 2 1 3 1
输出:
YES
NO
说明:
对于第一个样例可以对区间[1,4,1] 的每个数字加上2,即可把数列a转换成数列b
对于第二个样例没法做相应的操作
【解决思路及要点】
- 思路很多,这里采用差分数组的方法。
- 对数组做差,然后遍历数组检验是否可以转换。
- 注意转换的那段区间应该是连续的。
- 具体讨论在代码中有详细注释。
【解决代码】
public class Solution4 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int t=sc.nextInt();
while(t-->0){
int n=sc.nextInt();
int[] a=new int[n];
int[] b=new int[n];
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
for(int i=0;i<n;i++){
b[i]=sc.nextInt();
}
//差分数组
for (int i = 0; i < n; ++i){
a[i] = b[i] - a[i];
}
//左右指针
int left=-1,right=-1;
int k=0;
//标记是否成功转换
boolean flag=true;
for(int i=0;i<n;i++){
//a[i]属于相等的部分
if(a[i]!=0){
//更新左指针
if(left==-1){
left=i;
}
//更新右指针
if (right == -1 || right == i - 1){
right=i;
}else if(right!=-1 && right!=i-1){
//此时若右指针不在初始位置并且不在i-1的位置,说明需要转换的地方不连续,无法进行转换操作
System.out.println("NO");
flag=false;
break;
}
}else if(a[i]<0){
//此时说明数组b需要加,对a操作不可能转换
System.out.println("NO");
flag=false;
break;
}else if(a[i]>0){
//a数组要加,如果k不是初始值,也不和a[i]相等,也无法转换
if(k!=0 && k!=a[i]){
System.out.println("NO");
flag=false;
break;
}
k=a[i];
}
}
if(flag){
System.out.println("YES");
}
}
}
}
【题目剖析】
- 这道题应该算是比较简单的了,用差分数组可以很快的解决,不过要注意一下不满足转换条件的判断。
- 总体来说,这道题一遍过的可能性还是非常大的。
- 应该也是能在短时间内解决的题目。
题五:优惠券
【题目描述】
你有n种无门槛优惠券,每种优惠券有一个面值ai。当一件商品的售价≥ai时,你可以出示这种优惠券直接抵扣。抵扣后,优惠券不会被回收,可以继续使用。现在,你想要买m件商品,每件商品的售价是bi,请问你最少需要花费多少钱?
【输入描述】
第一行两个正整数n,m(1≤n,m≤10^6)
第二行n个正整数ai (0≤ai≤10^6),代表n种无门槛优惠券的面值 (不保证排序)
第三行m个正整数bi (0≤bi ≤10^6),代表m件商品的价格 (不保证排序)
【输出描述】
输出合理使用优惠券后,购买上述m件商品最少需要的花费。
【示例】
输入:
3 4
50 100 200
99 199 200 300
输出:
248
【解决思路及要点】
- 由于优惠券可以重复使用,那么这个题就很简单了。
- 只需要对优惠券数组进行排序,然后再用二分搜索找到最接近价格的优惠券即可。
- 注意一下:Java中BinarySearch返回的值的含义。
【解决代码】
public class Solution5 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int m=sc.nextInt();
int[] a=new int[n];
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
Arrays.sort(a);
long ans=0;
while(m-->0){
int price=sc.nextInt();
int index=Arrays.binarySearch(a,price);
//注意二分搜索的返回值
if(index<0){
index=-index-2;
}
ans+=price-a[index];
}
System.out.println(ans);
}
}
【题目剖析】
- 这道题非常简单,需要看清题目,并且对相关API熟悉。
- 应该是可以在极短时间内做出的。
该次笔试题感悟:
- 首先对这些题目的难度大体排一下,应该是:一,五,四,二,三。
- 说明不一定一个一个挨着做。
- 熟悉的模型是里面最有区分度的一个题,说明需要加强平时的硬编码能力。
- 两个小时的时间,一,四,五应该都是可以完美解决的,二可能需要花一点时间,三完全AC可能有些难度。
- 总体来说,这些题的难度还不是很大。
- 平时的积累一定要多回顾一下,不然碰到见过的模型,又忘了如何处理就比较麻烦了。
- 最后:好好积累,好好学!