一、题目描述
近些年来,我国防沙治沙取得显著成果。某沙漠新种植N棵胡杨(编号1~N),排成一排。一个月后,有M棵胡杨未能成活。现可补种胡杨K棵,请问如何补种(只能补种,不能新种),可以得到最多的连续胡杨树?
-
输入描述:
- N:总种植数量,1<=N<=100000。
- M:未成活胡杨数量,1<=M<=N。M个空格分隔的数,按编号从小到大排列。
- K:最多可以补种的数量,0<=K<=M。
-
输出描述:最多的连续胡杨棵树。
二、示例
-
示例1:
- 输入:5 2 2 4 1(表示总共5棵树,2棵未成活,分别是2号和4号,最多补种1棵)。
- 输出:3(补种到2或4结果一样,最多的连续胡杨棵树都是3)。
-
示例2:
- 输入:10 3 2 4 7 1(表示总共10棵树,3棵未成活,分别是2号、4号和7号,最多补种2棵)。
- 输出:6(补种第7棵树,最多连续胡杨树棵数为6,即5、6、7、8、9、10)。
三、解题思路
-
状态表示:
- 用0表示死树,1表示活着的树。
- 将题目转化为求含有K个0的最长子串。
-
采用滑动窗口:
- 每当窗口含有K个0时,求子串最大长度。
- 使用队列(Queue)存储每个0的下标,窗口有K个0时,将队列栈顶的下标更新为left。
四、解题步骤
- 读取输入数据,初始化相关变量。
- 创建一个长度为N+1的数组(为了方便处理,数组下标从1开始),初始化为全1(表示所有树都活着)。
- 根据输入的未成活胡杨编号,将数组中对应位置的元素置为0(表示死树)。
- 使用滑动窗口算法,遍历数组,记录窗口内0的数量,并动态调整窗口的大小。
- 当窗口内0的数量等于K时,记录当前窗口的大小(即连续活着的胡杨树的数量),并更新最大连续树木数量。
- 当窗口内0的数量超过K时,移动左指针缩小窗口,直到窗口内0的数量再次不超过K。
- 遍历结束后,输出最大连续树木数量。
五、代码实现(队列)
import java.util.Scanner;
import java.util.Queue;
import java.util.LinkedList;
public class PlantingTrees {
/**
* 找到可以补种得到最多的连续胡杨树的数量
*
* @param N 总种植数量
* @param M 未成活胡杨数量
* @param deadTrees 未成活胡杨的编号
* @param K 最多可以补种的数量
* @return 最多的连续胡杨树的数量
*/
public static int maxContinuousTrees(int N, int M, int[] deadTrees, int K) {
// 初始化数组
int[] trees = new int[N + 1];
for (int i = 1; i <= N; i++) {
trees[i] = 1; // 所有树都活着
}
// 标记未成活的胡杨
for (int tree : deadTrees) {
trees[tree] = 0;
}
// 使用滑动窗口算法
Queue<Integer> queue = new LinkedList<>();
int left = 1, right = 1;
int maxContinuous = 0;
while (right <= N) {
if (trees[right] == 0) {
queue.offer(right);
}
while (queue.size() > K) {
int deadIndex = queue.poll();
left = deadIndex + 1;
}
maxContinuous = Math.max(maxContinuous, right - left + 1);
right++;
}
return maxContinuous;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int M = scanner.nextInt();
int[] deadTrees = new int[M];
for (int i = 0; i < M; i++) {
deadTrees[i] = scanner.nextInt();
}
int K = scanner.nextInt();
scanner.close();
int result = maxContinuousTrees(N, M, deadTrees, K);
System.out.println(result);
}
}
六、代码实现(数组)
import java.util.Scanner;
public class PlantingTrees {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取输入
int N = scanner.nextInt();
int M = scanner.nextInt();
int[] deadTrees = new int[M];
for (int i = 0; i < M; i++) {
deadTrees[i] = scanner.nextInt();
}
int K = scanner.nextInt();
// 初始化胡杨数组并标记死树
boolean[] treesAlive = new boolean[N + 1];
for (int i = 1; i <= N; i++) {
treesAlive[i] = true;
}
for (int deadTree : deadTrees) {
treesAlive[deadTree] = false;
}
// 滑动窗口算法
int left = 1, right = 1;
int zeros = 0; // 窗口内死树的数量
int maxLength = 0; // 最大连续活树的数量
while (right <= N) {
if (!treesAlive[right]) {
zeros++;
}
// 缩小窗口直到死树数量不超过K
while (zeros > K) {
if (!treesAlive[left]) {
zeros--;
}
left++;
}
// 更新最大长度
maxLength = Math.max(maxLength, right - left + 1);
right++;
}
// 输出结果
System.out.println(maxLength);
scanner.close();
}
}
运行示例解析
首先,让我们解析一下输入示例5 2 2 4 1
:
N = 5
:总共有5棵胡杨。M = 2
:有2棵胡杨未成活。deadTrees = [2, 4]
:未成活胡杨的编号是2和4。K = 1
:最多可以补种1棵胡杨。
现在,让我们逐步分析代码的执行过程:
- 初始化
trees
数组,所有元素都设置为1(表示活着)。 - 将
deadTrees
数组中的值(2和4)在trees
数组中对应的位置设置为0(表示未成活)。 - 使用滑动窗口算法遍历
trees
数组。
但是,代码中的滑动窗口算法有几个问题:
- 当
trees[right]
为0时,将其索引添加到队列中。这是正确的。 - 但是,当队列中的死树数量超过
K
时,代码只是简单地从队列中移除一个死树的索引,并将left
设置为该索引的下一个位置。这是不正确的,因为这样做没有考虑到在left
和right
之间可能还有其他死树。此外,这样做可能会导致left
跳过一些可能形成更长连续活树序列的位置。
正确的做法应该是:
- 当队列中的死树数量超过
K
时,不断从队列中移除死树的索引,并递增left
,直到队列中的死树数量不超过K
为止。但是,left
的递增应该基于队列中移除的死树索引的下一个位置(如果队列不为空),或者如果队列为空,则left
可以直接设置为right
(尽管在这种情况下,由于窗口内没有超过K
个死树,所以通常不会发生这种情况)。 - 然而,更简单且高效的方法是使用两个计数器而不是队列来跟踪窗口内0(死树)和1(活树)的数量,因为题目只关心死树的数量是否超过
K
。