蓝桥云课-5. 花灯调整【算法赛】

目录

题目:

解题思路:

代码:

问题:

总结:


5.花灯调整【算法赛】 - 蓝桥云课

题目:

解题思路:

从前往后统计其后缀和,最后判断与当前位置翻转是否相同即可(偶数相当于没有翻转,奇数翻转)

代码:

java 复制代码
 /*out
*Stack Integer ArrayList String StringBuffer peek
*Collections imports LinkedList offer return
*empty polls offerLast pollFirst isEmpty
*List Deque append length HashMap
*return remove boolean continue charAt
*toString static System println nextInt
*Scanner  System  toCharArray contains
*/
import java.util.*;
public class Main {
    static Scanner sc = new Scanner(System.in);
    public static void main(String[] args) {
        int t=1;
        while(t--!=0){
          slove();
        }
        sc.close();
    }
    public static void slove(){
      int n=sc.nextInt(),k=sc.nextInt();
      String s=sc.next();
      int [] p=new int[n];
      for(int i=0;i<k;i++){
        p[sc.nextInt()-1]++;
      }
      int[] ciShu=new int[n];
      ciShu[n-1]=p[n-1];
      for(int i=n-2;i>=0;i--){
        ciShu[i]=p[i]+ciShu[i+1];
      }
      char[] tar=s.toCharArray();
      for(int i=0;i<tar.length;i++){
        int a=tar[i]-'0';
        if(a%2!=ciShu[i]%2){
          System.out.println("No");
          return ;
        }
      }
      System.out.println("Yes");
    }
}

问题:

总结:

【算法解析】后缀和解决 "花灯调整" 策略判定问题:从问题本质到代码实现

在算法竞赛中,"操作叠加的状态判定"类问题是高频考点之一 ------ 这类问题的核心是 "操作的顺序不影响最终状态,仅与操作次数的奇偶性有关"。本文以 "花灯调整" 问题为例,从问题拆解、核心规律、后缀和原理、代码实现等维度,全面讲解如何用后缀和高效解决这类问题,同时深入剖析代码的每一个细节,帮助你掌握这类问题的通用解法。

一、问题背景与需求拆解

在开始代码分析前,我们先彻底理解问题的规则与目标,这是后续算法设计的基础。

1.1 问题规则

初始状态:n盏花灯,全部处于关闭状态(用 0 表示);

游客操作:每位游客选择一个 "前缀p"(即从第 1 盏到第p盏灯的连续区间),将该区间内所有花灯的状态反转(0 变 1,1 变 0);

目标状态:给定一个长度为n的 01 字符串S,其中S[i]表示第i+1盏灯的目标状态(S[i]='1'表示亮,S[i]='0'表示灭);

操作顺序:游客可以任意调整操作的执行顺序;

问题目标:判断是否存在一种操作顺序,使得最终所有花灯的状态与目标串S完全一致。

1.2 需求转化为数学模型

我们的核心任务是:

验证 "游客操作对每盏灯的反转次数的奇偶性" 是否恰好等于 "从初始状态(0)到目标状态(S[i])所需的反转次数的奇偶性"。

原因很简单:

反转操作是幂等的(反转偶数次等价于未反转,反转奇数次等价于反转 1 次);

操作顺序不影响最终状态(因为反转是 "叠加" 的,先反转前 3 盏再反转前 5 盏,与先反转前 5 盏再反转前 3 盏,每盏灯的反转次数完全相同)。

二、核心规律:反转次数的统计与后缀和的应用

要解决问题,首先需要明确每盏灯的反转次数如何计算------ 这是问题的关键突破口。

2.1 每盏灯的反转次数的定义

对于第i盏灯(索引从 0 开始),它的反转次数 = 选择了 "前缀p ≥ i+1" 的游客数量之和(因为游客选前缀p时,会反转前p盏灯,第i盏灯属于前p盏的条件是p ≥ i+1)。

举个例子:

第 0 盏灯(对应实际第 1 盏)的反转次数 = 选了p≥1的游客数量之和;

第 2 盏灯(对应实际第 3 盏)的反转次数 = 选了p≥3的游客数量之和。

2.2 为什么用 "后缀和" 统计反转次数

统计 "选了p ≥ x的游客数量之和",本质是对 "游客选择的前缀数组" 做 "从 x 到末尾的累加"------ 这正是后缀和的典型应用场景:

前缀和:从左到右累加,统计 "前 x 项的和";

后缀和:从右到左累加,统计 "从 x 到末尾的和"。

因此,我们可以用后缀和快速计算每盏灯的反转次数,时间复杂度为O(n),完全满足n ≤ 10^5的规模要求。

三、代码整体结构解析

我们先从宏观上看代码的模块划分,理解代码是如何对应问题需求的:

java

运行

import java.util.*;

public class Main {

static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {

int t=1;

while(t--!=0){ // 兼容多组测试用例(此处t=1,仅执行1次)

slove();

}

sc.close();

}

public static void slove(){

// 1. 输入读取与初始化

int n=sc.nextInt(),k=sc.nextInt();

String s=sc.next();

int [] p=new int[n];

for(int i=0;i<k;i++){

p[sc.nextInt()-1]++;

}

// 2. 计算每盏灯的反转次数(后缀和)

int[] ciShu=new int[n];

ciShu[n-1]=p[n-1];

for(int i=n-2;i>=0;i--){

ciShu[i]=p[i]+ciShu[i+1];

}

// 3. 对比反转次数的奇偶性与目标状态

char[] tar=s.toCharArray();

for(int i=0;i<tar.length;i++){

int a=tar[i]-'0';

if(a%2!=ciShu[i]%2){

System.out.println("No");

return ;

}

}

// 4. 输出结果

System.out.println("Yes");

}

}

代码分为 4 个核心模块:

输入读取模块:将输入的 "前缀选择" 统计到数组中;

后缀和计算模块:用后缀和得到每盏灯的反转次数;

奇偶性对比模块:验证反转次数的奇偶性是否匹配目标状态;

输出模块:根据验证结果输出 "Yes" 或 "No"。

四、输入读取模块:结构化存储游客的选择

输入处理是算法题的基础,结构化存储能避免后续逻辑中 "变量混淆" 的问题,我们详细解析这部分代码的设计思路。

4.1 输入的对应关系

问题的输入分为三部分:

第一行:n(花灯数)、k(游客数);

第二行:目标串S(长度为n的 01 字符串);

第三行:k个整数p_1~p_k(每位游客选择的前缀)。

4.2 代码的输入处理逻辑

java

运行

// 读取n和k

int n=sc.nextInt(),k=sc.nextInt();

// 读取目标串s

String s=sc.next();

// 定义数组p:p[x]表示"选择了前缀x+1的游客数量"(x是0-based索引)

int [] p=new int[n];

for(int i=0;i<k;i++){

// 游客输入的p是1-based(比如选前缀3对应实际第3盏),转为0-based索引

int choose = sc.nextInt() - 1;

p[choose]++;

}

关键细节:

游客输入的 "前缀p" 是1-based(比如选 "前缀 3" 表示前 3 盏灯),而代码中数组是0-based(索引 0 对应实际第 1 盏),因此需要sc.nextInt() - 1将输入转为数组索引;

数组p的含义:p[x]表示 "选择了前缀x+1的游客数量"(比如p[2]表示选了前缀 3 的游客数)。

4.3 样例输入的存储结果

以样例输入为例:

plaintext

样例输入:

5 3

10010

4 3 1

n=5,k=3,目标串s="10010";

游客选择的前缀是 4、3、1,转为 0-based 索引是 3、2、0;

数组p的结果:p[0]=1(选前缀 1 的游客数)、p[2]=1(选前缀 3 的游客数)、p[3]=1(选前缀 4 的游客数),其余为 0 → p = [1,0,1,1,0]。

五、后缀和计算模块:核心逻辑的实现

这是代码的灵魂部分 ------ 用后缀和计算每盏灯的反转次数,我们从原理、代码、样例验证三个维度详细解析。

5.1 后缀和的原理回顾

后缀和的定义是:

对于数组arr,后缀和数组suffixSum满足:suffixSum[i] = arr[i] + suffixSum[i+1](从右到左计算)。

对于我们的问题,数组p存储了 "每个前缀被选择的次数",因此:

第i盏灯(0-based)的反转次数 = p[i] + p[i+1] + ... + p[n-1] → 这正是后缀和数组suffixSum[i]的值。

5.2 后缀和的代码实现

java

运行

// 定义ciShu数组:ciShu[i]表示第i盏灯的反转次数

int[] ciShu=new int[n];

// 初始化最后一盏灯的反转次数(只有选了前缀n的游客会影响它)

ciShu[n-1]=p[n-1];

// 从倒数第二盏灯往前计算后缀和

for(int i=n-2;i>=0;i--){

ciShu[i] = p[i] + ciShu[i+1];

}

代码逻辑拆解:

最后一盏灯(i=n-1)的反转次数 = 选了前缀n的游客数(即p[n-1]);

第i盏灯的反转次数 = 选了前缀i+1的游客数(p[i]) + 第i+1盏灯的反转次数(因为选前缀≥i+2的游客也会影响第i盏灯)。

5.3 样例的后缀和计算结果

以样例的p = [1,0,1,1,0]为例:

ciShu[4] = p[4] = 0(最后一盏灯,选前缀 5 的游客数为 0);

ciShu[3] = p[3] + ciShu[4] = 1 + 0 = 1(第 4 盏灯,选前缀 4 的游客数 + 第 5 盏的反转次数);

ciShu[2] = p[2] + ciShu[3] = 1 + 1 = 2(第 3 盏灯,选前缀 3 的游客数 + 第 4 盏的反转次数);

ciShu[1] = p[1] + ciShu[2] = 0 + 2 = 2(第 2 盏灯,选前缀 2 的游客数 + 第 3 盏的反转次数);

ciShu[0] = p[0] + ciShu[1] = 1 + 2 = 3(第 1 盏灯,选前缀 1 的游客数 + 第 2 盏的反转次数);

最终ciShu数组(每盏灯的反转次数)为:[3,2,2,1,0]。

六、奇偶性对比模块:判定是否可行的核心

这部分代码的作用是验证 "每盏灯的反转次数的奇偶性" 是否与 "目标状态所需的反转次数的奇偶性" 一致 ------ 这是问题的最终判定条件。

6.1 目标状态与反转次数的奇偶性

初始状态下,所有灯都是 0,要达到目标状态S[i](0 或 1):

若S[i] = '0':不需要反转,或反转偶数次(最终状态为 0);

若S[i] = '1':需要反转奇数次(最终状态为 1)。

因此,目标状态所需的反转次数的奇偶性 = S[i] - '0'(将字符转为整数 0 或 1)。

6.2 代码的奇偶性对比逻辑

java

运行

// 将目标串转为字符数组,方便逐位访问

char[] tar = s.toCharArray();

// 遍历每盏灯

for(int i=0;i<tar.length;i++){

// 将字符'0'/'1'转为整数0/1,得到目标所需的奇偶性

int targetParity = tar[i] - '0';

// 计算当前灯反转次数的奇偶性

int actualParity = ciShu[i] % 2;

// 若奇偶性不匹配,直接输出"No"并返回

if(targetParity != actualParity){

System.out.println("No");

return ;

}

}

6.3 样例的奇偶性对比验证

样例中:

目标串s="10010" → tar = ['1','0','0','1','0'];

目标所需的奇偶性:[1,0,0,1,0];

实际反转次数的奇偶性(ciShu % 2):[3%2=1, 2%2=0, 2%2=0, 1%2=1, 0%2=0];

两者完全一致,因此输出 "Yes",与样例结果匹配。

七、边界情况与性能分析

在算法题中,边界处理和性能是衡量代码质量的关键指标,我们分析代码在这两方面的表现。

7.1 边界情况的处理

代码能正确处理以下边界情况:

所有灯都是 0:目标所需的奇偶性全为 0,只需验证实际反转次数全为偶数;

所有灯都是 1:目标所需的奇偶性全为 1,只需验证实际反转次数全为奇数;

游客数量为 0:此时所有灯的反转次数为 0,只需目标串全为 0;

游客选择的前缀全为 n:此时所有灯的反转次数等于游客数量,只需验证所有灯的目标奇偶性相同。

7.2 性能分析

代码的时间复杂度为O(n + k):

输入处理:O(k)(遍历 k 个游客的选择);

后缀和计算:O(n)(遍历 n 盏灯);

奇偶性对比:O(n)(遍历 n 盏灯)。

空间复杂度为O(n):

数组p和ciShu的空间都是O(n),完全满足n ≤ 10^5的题目限制。

八、代码的优化与拓展

虽然当前代码已经能正确解决问题,但我们可以从可读性、扩展性角度做一些优化,让代码更通用。

8.1 变量名的语义化优化

原代码中ciShu(次数)的变量名不够直观,可优化为reverseCount(反转次数);p可优化为prefixChooseCount(前缀选择次数):

java

运行

// 优化后

int[] prefixChooseCount = new int[n];

for(int i=0;i<k;i++){

int choose = sc.nextInt() - 1;

prefixChooseCount[choose]++;

}

int[] reverseCount = new int[n];

reverseCount[n-1] = prefixChooseCount[n-1];

for(int i=n-2;i>=0;i--){

reverseCount[i] = prefixChooseCount[i] + reverseCount[i+1];

}

8.2 兼容多组测试用例

原代码中while(t--!=0)的t=1,仅支持单组测试用例。若要支持多组,只需将t改为sc.nextInt():

java

运行

public static void main(String[] args) {

int t = sc.nextInt(); // 读取测试用例数

while(t--!=0){

slove();

}

sc.close();

}

8.3 拓展:处理 "反转区间" 的通用问题

这类 "操作区间、统计每点的操作次数" 的问题,都可以用差分 + 前缀和的方式解决(后缀和是前缀和的一种变体)。例如:

若操作是 "反转区间[l, r]",可以用差分数组记录区间的变化,再用前缀和得到每点的操作次数。

九、总结

本文以 "花灯调整" 问题为例,详细讲解了后缀和在 "操作叠加的状态判定" 类问题中的应用:

问题拆解:将 "状态匹配" 转化为 "反转次数的奇偶性匹配";

核心规律:操作顺序不影响最终状态,仅与操作次数的奇偶性有关;

算法选择:用后缀和快速统计每盏灯的反转次数,时间复杂度O(n);

代码实现:输入处理→后缀和计算→奇偶性对比→输出结果,模块清晰。

这类问题的通用解题步骤是:

分析操作对每个元素的影响;

用前缀和 / 后缀和 / 差分统计每个元素的操作次数;

验证操作次数的奇偶性是否匹配目标状态。

掌握这一思路后,你可以轻松解决类似的 "开关灯""翻转字符串" 等问题 ------ 核心是抓住 "操作的幂等性" 和 "区间统计的前缀和 / 后缀和技巧"。

相关推荐
.小墨迹2 小时前
C++学习之std::move 的用法与优缺点分析
linux·开发语言·c++·学习·算法·ubuntu
努力学习的小廉2 小时前
【QT(五)】—— 常用控件(二)
开发语言·qt
wanghowie2 小时前
01.02 Java基础篇|核心数据结构速查
java·开发语言·数据结构
乂爻yiyao2 小时前
java并发演进图
java
java1234_小锋2 小时前
Redis6为什么引入了多线程?
java·redis
|晴 天|2 小时前
前端闭包:从概念到实战,解锁JavaScript高级技能
开发语言·前端·javascript
努力学算法的蒟蒻2 小时前
day38(12.19)——leetcode面试经典150
算法·leetcode·面试
9号达人2 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
看见繁华2 小时前
C++ 设计模式&设计原则
java·c++·设计模式