华为od机考2025A卷真题 -补种未成活胡杨

题目描述与示例

题目描述

近些年来,我国防沙治沙取得显著成果。某沙漠新种植N棵胡杨(编号1-N),排成一排。

一个月后,有M棵胡杨未能成活。

现可补种胡杨K棵,请问如何补种(只能补种,不能新种) ,可以得到最多的连续胡杨树?

题目练习网址:https://www.algomooc.com/problem/P3254

输入描述

复制代码
N`:总种植数量 `1<=N<=10``^5
M`:未成活胡杨数量 `1<=M<=N

M个空格分隔的数,按编号从小到大排列

复制代码
K`:最多可以补种的数量`0`` ``<=`` ``K`` ``<=`` ``M

输出描述

最多的连续胡杨棵树

示例一

输入

Plain 复制代码
5
2
2 4
1

输出

Plain 复制代码
3

说明

补种胡杨24,可以得到连续的胡杨[1, 2, 3][3, 4, 5]

示例二

输入

Plain 复制代码
10
3
2 4 7
1

输出

Plain 复制代码
6

说明

补种胡杨7,可以得到连续的胡杨[5, 6, 7, 8, 9, 10]

示例三

输入

Plain 复制代码
20
3
4 9 15
2

输出

Plain 复制代码
16

解题思路

不定滑窗解法

一种非常容易想到的做法是,对原来给定的数据进行平铺还原的操作。譬如对于示例二

Plain 复制代码
10
3
2 4 7
1

我们将这个长度为10的原数组平铺为

Python 复制代码
[1, 0, 1, 0, 1, 1, 0, 1, 1, 1]

其中0表示尚未种树,1表示已经种树。

注意,由于题目所给定的编号是1-N,而数组的下标是从0开始的,此处的编号会存在一个1的偏差。

这部分过程的代码如下

Python 复制代码
# 初始化平铺数组,初始化均为1
nums = [1] * N

# 遍历所有死树
for i in range(M):
    # trees[i]是死树从1到N的编号
    # trees[i]是死树在数组中从0到N-1的索引
    nums[trees[i]-1] = 0

由于k = 1,这意味着我们只能够最多种下一棵树。种下的这棵树,要使得数组存在最长的连续的1

所以问题就可以转变为,找到一个最长的连续子数组 ,这个子数组包含k0

因为这k0都将会被替换为1,将得到最长的连续1的个数。

譬如对于示例二而言,我们仅需将索引为6的未成活杨树进行补种,就可以得到最长的连续1的数组。

Python 复制代码
[1, 0, 1, 0, 1, 1, 0, 1, 1, 1]

直接使用滑窗三问三答就很简单了

Python 复制代码
# 初始化左指针为0,初始化窗口中包含的0的个数为0,初始化答案为0
left = 0
win_0 = 0
ans = 0

# 进行滑窗过程
for right, num in enumerate(nums):
    # A1
    if num == 0:
        win_0 += 1
    # A2
    while win_0 > K:
        if nums[left] == 0:
            win_0 -= 1
        left += 1
    # A3
    ans = max(ans, right - left + 1)

print(ans)

注意到上述做法的时间复杂度是O(N)的,当N取上限值10^5时,是可以通过所有用例的。

固定滑窗解法

固定滑窗的解法较抽象,但是时间复杂度可以达到更加优秀的O(M)的复杂度。感兴趣的同学可以自行学习。

从题目所给的几个例子可以看出,如果M远小于N那么那些原先已经连续成活的树木,完全可以只用区间长度来进行表示

譬如对于示例二,由于我们知道为未成活的杨树编号分别为

Python 复制代码
2 4 7

那么也就知道,原先就已经成活的杨树被分成了4部分(4个区间),其中数目分别为

Python 复制代码
1 1 2 3

这样就比不定滑窗解法中,将原先的数组进行平铺出来的这种做法

Python 复制代码
[1, 0, 1, 0, 1, 1, 0, 1, 1, 1]

进行了更进一步的数据压缩

这个结果可以用下面代码得到

Python 复制代码
# 构建tree_left哈希表用于表示某棵死树
# 与其左边最近的死树(上一棵死树)之间,一共有多少棵连续的活树
tree_left = dict()
# 第一棵死树为trees[0],由于编号是从1开始
# 所以其左边一共有trees[0] - 1棵连续的活树
tree_left[trees[0]] = trees[0] - 1
# 遍历除了第一棵死树外的所有死树
for i in range(1, M):
    # 当前死树,上一棵死树的编号分别为trees[i], trees[i-1]
    tree, pre_tree = trees[i], trees[i-1]
    # 两棵树之间的连续活树的数目为 tree - pre_tree - 1
    tree_left[tree] = tree - pre_tree - 1

# 构建tree_right哈希表用于表示某棵死树
# 与其右边最近的死树(下一棵死树)的之间,一共有多少棵连续的活树
tree_right = dict()
# 最后一棵死树为trees[-1],最后一棵树的编号为N
# 所以其右边一共有N - trees[-1]棵连续的活树
tree_right[trees[-1]] = N - trees[-1]
# 遍历除了最后一棵死树外的所有死树
for i in range(M-1):
    # 当前死树,下一棵死树的编号分别为trees[i], trees[i+1]
    tree, nxt_tree = trees[i], trees[i+1]
    # 两棵树之间的连续活树的数目为 nxt_tree - tree - 1
    tree_right[tree] = nxt_tree - tree - 1

其中tree_left表示,某个未成活杨树编号的左边,一共存在多少棵连续的活树。

同理tree_right表示,某个未成活杨树编号的右边,一共存在多少棵连续的活树。

譬如对于上述例子存在

Python 复制代码
tree_left = {
2: 1, 
4: 1, 
7: 2}

tree_right = {
2: 1, 
4: 2,
7: 3} 

由于每种下一棵树,能够使得每一个死树的左右两边的连续活树连接起来。

因此我们会考虑,如果将其中原先的M棵死树中的近邻的K棵进行种植,则可以尽可能长地得到连续的活树了。

这就退化成了一个固定滑窗的问题。

考虑原来的长度为M的死树数组trees,选择其中连续的K个元素将其转为活树,使得连续的活树数目尽可能的多。求最大的连续活树的数目。

显然,每一个固定滑窗中连续存活的活树数目由三部分构成:

  1. K棵补种的活树
  2. K棵补种的活树中的最左边那棵补种的树,其左边连续的活树数目
  3. K棵补种的活树的每一棵树,其右边连续的活树数目

因此第一个固定滑窗的初始化为

Python 复制代码
win_sum = K + tree_left[trees[0]] + sum(tree_right[i] for i in trees[:K])

而每一次窗口移动的时候,最右边新补种的树的右边,要将加上tree_right[right]棵新种下的树。(A1)

而最左边原先补种的树不再种植,那么这部分原先存在窗口中的树tree_left[left]将被删去。(A2)

考虑固定滑窗三问三答,代码为

Python 复制代码
# 考虑第一个固定滑窗的情况,这个滑窗包含了最开始的K棵死树,如果把这些树都种下
# 第一个固定滑窗中的连续成活胡杨的数目由以下部分构成:
# 1. K棵补种的树
# 2. 第一棵死树trees[0]左边所有连续成活的树
# 3. 这K棵死树右边的连续成活的树
# 上述部分进行求和,即第一个固定滑窗的连续成活胡杨总和,用变量win_sum表示
win_sum = K + tree_left[trees[0]] + sum(tree_right[i] for i in trees[:K])
# 初始化答案变量为第一个固定滑窗的结果
ans = win_sum

# 进行固定滑窗过程
for right, idx in enumerate(trees[K:], K):
    # Q1:对于每一个右指针right所指的元素idx,做什么操作?
    # A1:将idx右边的连续成活胡杨,加入窗口和win_sum中
    win_sum += tree_right[idx]
    # Q2:对于left所指的元素idx_left,要做什么操作?
    # A2:将idx_left左边的连续成活胡杨,从窗口和win_sum中减出
    left = right-K
    idx_left = trees[left]
    win_sum -= tree_left[idx_left]
    # Q3:如何更新ans?
    # A3:取ans和win_sum中的较大值进行更新
    ans = max(win_sum, ans)

代码

解法一:不定滑窗

Python

Python 复制代码
# 欢迎来到「欧弟算法 - 华为OD全攻略」,收录华为OD题库、面试指南、八股文与学员案例!
# 地址:https://www.odalgo.com

# 胡杨总数
N = int(input())
# 未成活数目
M = int(input())
# 未成活胡杨的位置
trees = list(map(int, input().split()))
# 补种数目
K = int(input())

# 初始化平铺数组,初始化均为1
nums = [1] * N

# 初始化平铺数组,初始化均为1
nums = [1] * N

# 遍历所有死树
for i in range(M):
    # trees[i]是死树从1到N的编号
    # trees[i]-1是死树在数组中从0到N-1的索引
    nums[trees[i]-1] = 0

# 初始化左指针为0,初始化窗口中包含的0的个数为0,初始化答案为0
left = 0
win_0 = 0
ans = 0

# 进行滑窗过程
for right, num in enumerate(nums):
    # A1
    if num == 0:
        win_0 += 1
    # A2
    while win_0 > K:
        if nums[left] == 0:
            win_0 -= 1
        left += 1
    # A3
    ans = max(ans, right - left + 1)

# 输出答案
print(ans)

Java

Java 复制代码
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 胡杨总数
        int N = scanner.nextInt();
        // 未成活数目
        int M = scanner.nextInt();
        // 未成活胡杨的位置
        int[] trees = new int[M];
        for (int i = 0; i < M; i++) {
            trees[i] = scanner.nextInt();
        }
        // 补种数目
        int K = scanner.nextInt();

        // 初始化平铺数组,初始化均为1
        int[] nums = new int[N];
        for (int i = 0; i < N; i++) {
            nums[i] = 1;
        }

        // 遍历所有死树
        for (int i = 0; i < M; i++) {
            // trees[i]是死树从1到N的编号
            // trees[i]-1是死树在数组中从0到N-1的索引
            nums[trees[i] - 1] = 0;
        }

        // 初始化左指针为0,初始化窗口中包含的0的个数为0,初始化答案为0
        int left = 0;
        int win_0 = 0;
        int ans = 0;

        // 进行滑窗过程
        for (int right = 0; right < N; right++) {
            // A1
            if (nums[right] == 0) {
                win_0++;
            }
            // A2
            while (win_0 > K) {
                if (nums[left] == 0) {
                    win_0--;
                }
                left++;
            }
            // A3
            ans = Math.max(ans, right - left + 1);
        }

        // 输出答案
        System.out.println(ans);
    }
}

C++

C++ 复制代码
#include <iostream>
#include <vector>

using namespace std;

int main() {
    // 胡杨总数
    int N;
    cin >> N;
    
    // 未成活数目
    int M;
    cin >> M;
    
    // 未成活胡杨的位置
    vector<int> trees(M);
    for (int i = 0; i < M; i++) {
        cin >> trees[i];
    }
    
    // 补种数目
    int K;
    cin >> K;
    
    // 初始化平铺数组,初始化均为1
    vector<int> nums(N, 1);
    
    // 遍历所有死树
    for (int i = 0; i < M; i++) {
        // trees[i]是死树从1到N的编号
        // trees[i]-1是死树在数组中从0到N-1的索引
        nums[trees[i] - 1] = 0;
    }
    
    // 初始化左指针为0,初始化窗口中包含的0的个数为0,初始化答案为0
    int left = 0;
    int win_0 = 0;
    int ans = 0;
    
    // 进行滑窗过程
    for (int right = 0; right < N; right++) {
        // A1
        if (nums[right] == 0) {
            win_0++;
        }
        // A2
        while (win_0 > K) {
            if (nums[left] == 0) {
                win_0--;
            }
            left++;
        }
        // A3
        ans = max(ans, right - left + 1);
    }
    
    // 输出答案
    cout << ans << endl;
    
    return 0;
}

时空复杂度

时间复杂度:O(N)。仅需一次遍历平铺后的数组,每一个元素只会被rightleft遍历到至多2次。

空间复杂度:O(1)。仅需若干常数变量

解法二:固定滑窗

Python

Python 复制代码
# 欢迎来到「欧弟算法 - 华为OD全攻略」,收录华为OD题库、面试指南、八股文与学员案例!
# 地址:https://www.odalgo.com



# 胡杨总数
N = int(input())
# 未成活数目
M = int(input())
# 未成活胡杨的位置
trees = list(map(int, input().split()))
# 补种数目
K = int(input())


# 构建tree_left哈希表用于表示某棵死树
# 与其左边最近的死树(上一棵死树)之间,一共有多少棵连续的活树
tree_left = dict()
# 第一棵死树为trees[0],由于编号是从1开始
# 所以其左边一共有trees[0] - 1棵连续的活树
tree_left[trees[0]] = trees[0] - 1
# 遍历除了第一棵死树外的所有死树
for i in range(1, M):
    # 当前死树,上一棵死树的编号分别为trees[i], trees[i-1]
    tree, pre_tree = trees[i], trees[i-1]
    # 两棵树之间的连续活树的数目为 tree - pre_tree - 1
    tree_left[tree] = tree - pre_tree - 1

# 构建tree_right哈希表用于表示某棵死树
# 与其右边最近的死树(下一棵死树)的之间,一共有多少棵连续的活树
tree_right = dict()
# 最后一棵死树为trees[-1],最后一棵树的编号为N
# 所以其右边一共有N - trees[-1]棵连续的活树
tree_right[trees[-1]] = N - trees[-1]
# 遍历除了最后一棵死树外的所有死树
for i in range(M-1):
    # 当前死树,下一棵死树的编号分别为trees[i], trees[i+1]
    tree, nxt_tree = trees[i], trees[i+1]
    # 两棵树之间的连续活树的数目为 nxt_tree - tree - 1
    tree_right[tree] = nxt_tree - tree - 1

# 考虑第一个固定滑窗的情况,这个滑窗包含了最开始的K棵死树,如果把这些树都种下
# 第一个固定滑窗中的连续成活胡杨的数目由以下部分构成:
# 1. K棵补种的树
# 2. 第一棵死树trees[0]左边所有连续成活的树
# 3. 这K棵死树右边的连续成活的树
# 上述部分进行求和,即第一个固定滑窗的连续成活胡杨总和,用变量win_sum表示
win_sum = K + tree_left[trees[0]] + sum(tree_right[i] for i in trees[:K])
# 初始化答案变量为第一个固定滑窗的结果
ans = win_sum

# 进行固定滑窗过程
for right, idx in enumerate(trees[K:], K):
    # Q1:对于每一个右指针right所指的元素idx,做什么操作?
    # A1:将idx右边的连续成活胡杨,加入窗口和win_sum中
    win_sum += tree_right[idx]
    # Q2:对于left所指的元素idx_left,要做什么操作?
    # A2:将idx_left左边的连续成活胡杨,从窗口和win_sum中减出
    left = right-K
    idx_left = trees[left]
    win_sum -= tree_left[idx_left]
    # Q3:如何更新ans?
    # A3:取ans和win_sum中的较大值进行更新
    ans = max(win_sum, ans)


print(ans)

C++

C++ 复制代码
#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;

int main() {
    int N, M, K;
    cin >> N >> M; // 胡杨总数、未成活数目
    vector<int> trees(M); // 未成活胡杨的位置
    for (int i = 0; i < M; i++) {
        cin >> trees[i];
    }
    cin >> K; // 补种数目

    unordered_map<int, int> treeLeft; // 构建tree_left哈希表用于表示某棵死树
    treeLeft[trees[0]] = trees[0] - 1; // 第一棵死树为trees[0],由于编号是从1开始,所以其左边一共有trees[0] - 1棵连续的活树
    for (int i = 1; i < M; i++) {
        int tree = trees[i]; // 当前死树
        int preTree = trees[i - 1]; // 上一棵死树的编号
        treeLeft[tree] = tree - preTree - 1; // 两棵树之间的连续活树的数目为 tree - pre_tree - 1
    }

    unordered_map<int, int> treeRight; // 构建tree_right哈希表用于表示某棵死树
    treeRight[trees[M - 1]] = N - trees[M - 1]; // 最后一棵死树为trees[-1],最后一棵树的编号为N,所以其右边一共有N - trees[-1]棵连续的活树
    for (int i = 0; i < M - 1; i++) {
        int tree = trees[i]; // 当前死树
        int nxtTree = trees[i + 1]; // 下一棵死树的编号
        treeRight[tree] = nxtTree - tree - 1; // 两棵树之间的连续活树的数目为 nxt_tree - tree - 1
    }

    int winSum = K + treeLeft[trees[0]]; // 考虑第一个固定滑窗的情况,这个滑窗包含了最开始的K棵死树,如果把这些树都种下
    for (int i = 0; i < K; i++) {
        winSum += treeRight[trees[i]]; // 第一个固定滑窗中的连续成活胡杨的数目由以下部分构成:1. K棵补种的树 2. 第一棵死树trees[0]左边所有连续成活的树 3. 这K棵死树右边的连续成活的树
    }
    int ans = winSum; // 初始化答案变量为第一个固定滑窗的结果

    for (int right = K, left = 0; right < M; right++, left++) {
        int idx = trees[right]; // 对于每一个右指针right所指的元素idx,将idx右边的连续成活胡杨,加入窗口和win_sum中
        winSum += treeRight[idx];

        int idxLeft = trees[left]; // 对于left所指的元素idxLeft,将idxLeft左边的连续成活胡杨,从窗口和win_sum中减出
        winSum -= treeLeft[idxLeft];

        ans = max(ans, winSum); // 更新ans,取ans和win_sum中的较大值进行更新
    }

    cout << ans << endl;
    return 0;
}

时空复杂度

时间复杂度:O(``M``)。构建哈希表和滑窗过程,均需要遍历M棵未成活胡杨树。

空间复杂度:O(``M``)。两个哈希表所占空间。