目录

题目描述

605. 种花问题

题意分析

一条花坛用数组 flowerbed 表示,0 是空地、1 是已经种了花。规则是任意两朵花不能相邻(原有的花已经满足这个规则)。给定还要新种 n 朵,问能不能种下。

「不能相邻」这条约束是局部的:某个位置能不能种,只取决于它自己和它左右各一格,跟更远的位置毫无关系。这一点非常关键——它意味着不需要任何全局信息,一趟从左到右的扫描就足以做出所有决策。

注意返回值是布尔而不是「最多能种几朵」。这给了一个小优化空间(种够 n 朵就可以提前返回真),但也带来一个陷阱:n 可能是 0,此时不种任何花就已经满足,必须返回真。

边界情形值得单独想清楚:数组的第一个位置没有左邻居、最后一个位置没有右邻居。正确的处理是把越界的一侧视作空地——花坛之外不会有花,所以那一侧的约束天然满足。若把越界当成「有花」,[0] 这种单格花坛就会被判成种不了,而正确答案是能种一朵。

约束方面,数组长度是线性规模,n 也不超过数组长度量级,显然目标是 $O(m)$ 一趟扫描、$O(1)$ 额外空间。题目没有任何暗示需要动态规划——事实上「相邻不能同时选、求最多能选几个」确实是打家劫舍式的 DP 模型,但因为这里每个位置的收益都是 1(而不是各不相同的权值),最优解可以用贪心直接构造出来,不必上 DP。

还有一处容易忽略的隐含前提:题目保证输入的 flowerbed 本身是合法的(不存在两朵相邻的花)。所以扫描时只需保证新种的花不破坏规则,不需要校验原有布局。

解法:贪心扫描

核心思路

先看这个问题的本质:把花坛里连续的空地段挑出来,在每段里尽量多种花,且不能与段两端已有的花相邻。这本质上是「相邻不可同选、求最大独立集」的一维版本,用打家劫舍那套 DP(dp[i] = max(dp[i-1], dp[i-2] + 1))当然能做,$O(m)$ 时间、$O(1)$ 空间。但对本题来说这是多余的:每个位置的收益都是 1,完全同质,不存在「牺牲眼前换取更大收益」的可能,所以最优解可以贪心地一步步构造出来。

贪心策略是:从左往右扫,遇到能种的位置立刻种下

为什么这样不会更差?关键在于「种在越靠左的位置,对右边的封锁越少」。假设某个最优方案在位置 j 种了一朵花,而贪心在更靠左的 ii < j)种下了一朵。由于扫描到 i 时判定为可种,说明 i-1ii+1 三格都空;把最优方案里 j 那朵挪到 i,只可能解除 j 附近的封锁、不会新增任何冲突(i 处本来就合法),因此改造后的方案花数不变且仍然合法。反复施行这个交换,任何最优方案都能被改造成贪心产出的方案,所以贪心结果就是最优。

具体到实现,判定条件是三个格子同时为空:当前位置 flowerbed[i] == 0、左邻居空或不存在、右邻居空或不存在。「不存在即视为空」用短路表达式 i == 0 || flowerbed[i-1] == 0 一行写完,既避免了越界访问,也省掉了为首尾各写一段特判。

种下之后必须真的把 flowerbed[i] 改成 1。这一步不是可选的记账,而是让后续判断正确的必要动作:下一个位置 i+1 会检查它的左邻居,如果不写回,它会以为左边仍是空地从而种下第二朵,直接违反规则。换句话说,数组本身充当了「已做决策」的状态载体,这是原地贪心的常见手法。

循环不变量:扫描到位置 i 时,flowerbed[0..i] 这一前缀已经是合法布局,且 count 等于其中新种的花数;同时这个前缀上的新种花数已达到该前缀所能容纳的最大值。扫完全程后 count 就是全花坛最多能新种的朵数,与 n 比较即得答案。

解题步骤

  • 准备计数器 count = 0,取出数组长度 len为什么count 统计的是新种的花数,不含原有的花,所以从 0 起算;长度提到循环外取,避免在越界判断里反复求值。
  • 从左到右遍历每个位置 i为什么:贪心要求「尽早种下」,方向必须是从左往右;顺序一旦反过来(从右往左)结论依然成立(对称),但绝不能左右横跳,否则「已决策前缀」这个不变量就不成立了。
  • flowerbed[i] != 0 时直接跳过为什么:该位置已经有花,既不能再种、也不需要处理;提前 continue 让后面的判断只需要关心空地这一种情形。
  • 判断左侧:i == 0 || flowerbed[i - 1] == 0为什么i == 0 时左边是花坛外部,不可能有花,视作空地;短路求值保证了 i == 0 时不会去访问 flowerbed[-1],两个目的一行达成。
  • 判断右侧:i == len - 1 || flowerbed[i + 1] == 0为什么:同理,末尾位置的右边是花坛外部,视作空地;短路顺序不能反,写成 flowerbed[i + 1] == 0 || i == len - 1 会先越界再判断。
  • 两侧都空时,把 flowerbed[i] 置为 1 并让 count++为什么:置 1 是必须的——下一轮 i + 1 会读取它作为左邻居,不写回就会连着种两朵;count++ 记录新增的花数。两个动作必须成对出现。
  • 循环结束后返回 count >= n为什么count 是最多能新种的朵数,只要不少于需求就返回真;用 >= 而不是 ==,因为种得比需要的多同样满足条件。n == 0 时这个式子恒为真,边界自然覆盖。

flowerbed = [1, 0, 0, 0, 1], n = 1 走一遍(正确答案是 true)。初始 count = 0len = 5

i = 0flowerbed[0] == 1,跳过。

i = 1:值为 0。左侧 i != 0,检查 flowerbed[0] == 1,非空——leftEmpty 为假,不种。

i = 2:值为 0。左侧 flowerbed[1] == 0 为空;右侧 flowerbed[3] == 0 为空。两侧都空,种下!flowerbed 变成 [1, 0, 1, 0, 1]count = 1

i = 3:值为 0。左侧 flowerbed[2] 此刻已经是 1——正是刚才写回的那朵——leftEmpty 为假,不种。如果上一步只加计数而不写回数组,这里会误判为可种,得到 count = 2 并且布局里出现相邻的两朵花。

i = 4:值为 1,跳过。

返回 1 >= 1true

再看同一个花坛但 n = 2count 仍然是 1,返回 1 >= 2false,符合预期。

最后看边界例子 flowerbed = [0], n = 1i = 0 时值为 0,左侧因 i == 0 直接判为空,右侧因 i == len - 1 直接判为空,于是种下,count = 1,返回 true。若把越界一侧当成「有花」,这里会返回 false——这就是「越界视作空地」这条规则的价值。

代码实现

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        // count 只统计新种的花,不含原有的花。
        int count = 0;
        int len = flowerbed.length;

        for (int i = 0; i < len; i++) {
            if (flowerbed[i] != 0) {
                continue;
            }
            // 短路求值:越界一侧视作空地,同时避免访问越界下标。
            boolean leftEmpty = i == 0 || flowerbed[i - 1] == 0;
            boolean rightEmpty = i == len - 1 || flowerbed[i + 1] == 0;
            if (leftEmpty && rightEmpty) {
                // 必须写回数组:下一轮要靠它判断左邻居,否则会连种两朵。
                flowerbed[i] = 1;
                count++;
            }
        }

        return count >= n;
    }
}
func canPlaceFlowers(flowerbed []int, n int) bool {
    // count 只统计新种的花,不含原有的花。
    count := 0
    length := len(flowerbed)

    for i := 0; i < length; i++ {
        if flowerbed[i] != 0 {
            continue
        }
        // 短路求值:越界一侧视作空地,同时避免访问越界下标。
        leftEmpty := i == 0 || flowerbed[i-1] == 0
        rightEmpty := i == length-1 || flowerbed[i+1] == 0
        if leftEmpty && rightEmpty {
            // 必须写回数组:下一轮要靠它判断左邻居,否则会连种两朵。
            flowerbed[i] = 1
            count++
        }
    }

    return count >= n
}

复杂度分析

  • 时间复杂度:$O(m)$,m 为花坛长度。凭什么:只有一层从左到右的循环,每个位置做常数次比较与至多两次赋值;没有回退、没有嵌套循环,每个格子恰好被作为「当前位置」访问一次、被作为「邻居」读取至多两次。
  • 空间复杂度:$O(1)$。凭什么:只用了 countleni 和两个布尔临时量,全是标量;花的种植状态直接写回入参数组,没有额外分配。代价是修改了入参——若调用方不允许原地修改,可以改用一个变量记住「上一个位置是否被新种」来替代写回,空间仍是 $O(1)$。

关键点总结

  • 约束是局部的(只看左右各一格)时,就可以一趟线性扫描解决,不需要任何全局结构。判断一道题该用扫描还是 DP,先问「当前决策依赖多远的信息」。
  • 「相邻不可同选、求最多选几个」本是打家劫舍式 DP,但当每个位置的收益完全相同时,贪心即最优。收益一旦各不相同(比如每个位置能种的花数不一样),贪心立刻失效、必须回到 DP——能说出这条分界线,比会写代码更重要。
  • 贪心的正确性要用交换论证说明:把最优解里靠右的选择往左挪到贪心的位置,方案数不变且仍合法,因此贪心不劣于任何最优解。面试中被追问「凭什么能贪」,这就是标准答法。
  • 原地修改数组来记录已做决策,是一维贪心的常用手法。写回这一步不是记账而是逻辑必需——后续判断要读它。凡是「决策会影响后续判定」的扫描,都要检查状态有没有真正落地。
  • 越界侧视作空地,用 i == 0 || ... 的短路表达式一行搞定,既是语义正确(花坛外不会有花),也避免了越界访问。短路的顺序不能颠倒。
  • 面试延伸:被追问「不许修改入参怎么办」时,答案是用一个布尔变量记住上一格是否刚被种下,或者按「连续空地段长度 L 能种 $\lfloor (L+1)/2 \rfloor$ 朵」的公式分段计算;被追问「能否提前退出」时,可以在 count >= n 时立刻返回真,最坏复杂度不变但平均更快。

易错点总结

  • 种花时只 count++ 而不把 flowerbed[i] 置为 1flowerbed = [0, 0], n = 2i = 0 种下后没有写回,i = 1 仍以为左邻是空地也种下,count = 2 返回 true;但这两朵花相邻并不合法,实际最多只能种 1 朵,正确答案是 false
  • 把越界一侧当成「有花」flowerbed = [0], n = 1 → 左右都被判为不可用,返回 false,正确答案是 true
  • 短路顺序写反成 flowerbed[i - 1] == 0 || i == 0i = 0 → 先访问 flowerbed[-1],直接数组越界抛异常。
  • 右邻界判断写成 i == len 而不是 i == len - 1i 走到最后一格 → 条件不成立,转而访问 flowerbed[len],越界崩溃。
  • 忘记先判断 flowerbed[i] != 0flowerbed = [1, 0, 1] → 在 i = 0 处发现左侧越界为空、右侧 flowerbed[1] == 0 也为空,于是在已有花的位置又"种"一朵,count 虚增。
  • 返回 count == n 而不是 count >= nflowerbed = [0, 0, 0, 0, 0], n = 1 → 最多能种 3 朵,3 == 1 为假,返回 false,而正确答案是 true
  • 没有处理 n == 0:若代码里写了 if (flowerbed.length == 0) return false 之类的前置分支 → n = 0 时本该返回 true(不种也满足),却返回 false
  • dp[i] = max(dp[i-1], dp[i-2] + 1) 却忽略原有的花flowerbed = [1, 0, 0, 0, 1] → DP 若不把已有花位置强制为「不可选且封锁邻居」,会算出能种 2 朵,正确答案是 1 朵。
  • 试图用「连续空地段长度 L 能种 L / 2 朵」的公式:段长为 3 的中间空地(两端有花)→ 公式给出 1,正确;但对首尾开放的段(如整个数组全空、长度 3)→ 公式给出 1,而实际能种 2 朵。正确公式要区分段两端是否邻花,直接扫描反而不易错。
  • 在循环中提前 return true 却把判断写在种花之前count 尚未包含本轮 → n 恰好等于最终朵数时会漏判一轮,返回 false

相似题目

题目 难度 考察点
198. 打家劫舍 中等 同为「相邻不可同选」,但每间房收益不同,贪心失效必须用 DP
213. 打家劫舍 II 中等 在 198 基础上首尾相连成环,需拆成两条线性链分别求解再取最大
55. 跳跃游戏 中等 同为一趟贪心扫描,但维护的是「能到达的最远下标」而非逐格判定
45. 跳跃游戏 II 中等 求最少步数,贪心要按「当前层能覆盖的右边界」分层推进
452. 用最少数量的箭引爆气球 中等 同样用交换论证证明贪心,但需先按右端点排序才能保证「尽早射」最优
435. 无重叠区间 中等 与 452 互为补集,考的是「保留最多不重叠区间」的等价转换
763. 划分字母区间 中等 需先预处理每个字母的最后出现位置,再用贪心扫描切分,依赖全局信息
134. 加油站 中等 贪心结论更隐蔽:总量为正必有解,且失败点之后的位置才可能是起点