这不仅仅是一个算法题,它更考验的是你对 Android UI 更新机制、多线程以及性能优化的综合理解。

题目解析:什么是“机器人点灯”?
核心场景:
想象一个界面,上面有一个由 N x M 个小灯泡(或格子)组成的网格,每个灯泡都有两种状态:亮(开) 或 灭(关)。
初始状态: 所有灯泡都是灭的。
操作规则:
你有一个机器人,它可以从网格的任意一个位置 (i, j) 开始,当机器人“点”一下某个位置的灯泡时,会发生连锁反应:
- 目标灯泡状态反转:被点中的灯泡,状态会反转(亮变灭,灭变亮)。
- 相邻灯泡状态反转:与目标灯泡上、下、左、右四个方向相邻的灯泡,状态也会反转。
- 对角线灯泡不受影响:左上、右上、左下、右下的灯泡状态不变。 标:** 给定一个目标状态(一个所有灯泡都亮的网格),如何找到最少的“点灯”步骤,或者如何用代码模拟出点亮所有灯泡的过程?
技术挑战:为什么这个问题在 Android 中很重要?
这个问题的难点不在于算法本身(它是一个经典的广度优先搜索/BFS问题),而在于如何在 Android 中高效、正确地更新 UI。

主要挑战:
-
性能问题:
- 网格大小可能很大(10x10 = 100 个灯泡)。
- 每次点击,需要修改 1 个自身和 4 个相邻灯泡的状态,共 5 个灯泡。
- 如果使用暴力搜索,计算量会非常大,BFS 会遍历所有可能的“状态组合”,状态空间是
2^(N*M),这是一个天文数字,我们需要优化的算法,或者至少在 UI 上做好优化,避免卡顿。
-
UI 更新线程问题:
- 这是最核心的 Android 知识点,Android 的 UI 操作(如修改 View 的背景色、设置文本)必须在主线程(UI 线程)上执行。
- 搜索算法(BFS)是一个计算密集型任务,如果放在主线程上运行,它会阻塞主线程,导致界面卡顿、无响应,甚至出现“应用无响应”(ANR)错误。
- 我们必须将耗时计算放在后台线程(如
AsyncTask、Thread、ExecutorService或Coroutines)中执行。
-
线程间通信问题:
(图片来源网络,侵删)- 后台线程计算完成后,得到了下一步需要点的灯泡坐标,如何安全地将这个结果通知给主线程,以便更新 UI?
- Android 提供了多种机制来解决这个问题,如
runOnUiThread()、Handler、LiveData(MVVM) 或Coroutines的withContext(Dispatcher.Main)。
解决方案与代码实现
我们将采用 “后台计算 + 主线程更新” 的模式,这里我们使用 Kotlin Coroutines,因为它更简洁、更现代,是 Android 开发的首选。
第 1 步:创建 UI 布局
我们创建一个 GridView 或 RecyclerView 来展示灯泡网格,这里为了简单,我们使用 GridLayout。
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="机器人点灯"
android:textSize="24sp"
android:layout_marginBottom="20dp"/>
<GridLayout
android:id="@+id/grid_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="5"
android:rowCount="5"
android:background="#333333">
</GridLayout>
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="开始求解"/>
</LinearLayout>
第 2 步:创建灯泡单元格
我们需要一个自定义的 View 来代表单个灯泡。
bulb_view.xml (作为背景):
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#888888" /> <!-- 默认灰色(灭) -->
<size
android:width="40dp"
android:height="40dp" />
</shape>
BulbView.kt (自定义 View):
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
class BulbView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
private var isOn: Boolean = false
init {
// 初始状态为灭
updateState()
}
fun turnOn() {
if (!isOn) {
isOn = true
updateState()
}
}
fun turnOff() {
if (isOn) {
isOn = false
updateState()
}
}
fun toggle() {
isOn = !isOn
updateState()
}
private fun updateState() {
// 根据状态设置不同的背景
if (isOn) {
setImageResource(R.drawable.bulb_on) // 亮灯的图片,例如黄色
} else {
setImageResource(R.drawable.bulb_off) // 灭灯的图片,例如灰色
}
}
}
(注意:你需要准备 bulb_on.xml 和 bulb_off.xml 两个 drawable 文件,或者直接使用颜色资源)
第 3 步:在 Activity 中实现逻辑
MainActivity.kt:
import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.GridLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.LinkedList
import java.util.Queue
class MainActivity : AppCompatActivity() {
private lateinit var gridLayout: GridLayout
private lateinit var bulbViews: Array<Array<BulbView>>
private val rows = 5
private val cols = 5
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
gridLayout = findViewById(R.id.grid_layout)
val btnStart = findViewById<Button>(R.id.btn_start)
// 初始化网格
initializeGrid()
btnStart.setOnClickListener {
// 点击按钮,开始求解
startSolving()
}
}
private fun initializeGrid() {
gridLayout.rowCount = rows
gridLayout.columnCount = cols
bulbViews = Array(rows) { Array(cols) { BulbView(this) } }
for (i in 0 until rows) {
for (j in 0 until cols) {
val bulb = bulbViews[i][j]
val params = GridLayout.LayoutParams().apply {
width = 80
height = 80
margin = 4
}
gridLayout.addView(bulb, params)
}
}
}
private fun startSolving() {
// 使用协程,在 IO 线程进行计算,完成后在 Main 线程更新 UI
lifecycleScope.launch {
val solutionSteps = findSolutionInWorkerThread()
animateSolution(solutionSteps)
}
}
/**
* 在后台线程中执行耗时的 BFS 算法。
* @return 一个步骤列表,每个步骤是一个需要点击的 (row, col) 坐标。
*/
private suspend fun findSolutionInWorkerThread(): List<Pair<Int, Int>> = withContext(Dispatchers.IO) {
// ... BFS 算法实现 ...
// 这里我们简化一下,假设算法找到了解
// 实际项目中,这里应该是完整的 BFS 逻辑
val solution = mutableListOf<Pair<Int, Int>>()
// 示例:一个简单的解法(可能不是最优)
solution.add(Pair(0, 0))
solution.add(Pair(1, 1))
solution.add(Pair(2, 2))
return@withContext solution
}
/**
* 在主线程中,逐步执行动画,展示解决方案。
*/
private suspend fun animateSolution(steps: List<Pair<Int, Int>>) = withContext(Dispatchers.Main) {
// 重置所有灯泡为灭
resetAllBulbs()
// 模拟每一步的点击
for (step in steps) {
val (row, col) = step
// 执行一次点击操作
performClick(row, col)
// 延迟一下,以便观察动画效果
kotlinx.coroutines.delay(500) // 延迟 500 毫秒
}
}
private fun resetAllBulbs() {
for (i in 0 until rows) {
for (j in 0 until cols) {
bulbViews[i][j].turnOff()
}
}
}
private fun performClick(row: Int, col: Int) {
// 1. 切换被点击的灯泡
bulbViews[row][col].toggle()
// 2. 切换上方的灯泡
if (row > 0) bulbViews[row - 1][col].toggle()
// 3. 切换下方的灯泡
if (row < rows - 1) bulbViews[row + 1][col].toggle()
// 4. 切换左边的灯泡
if (col > 0) bulbViews[row][col - 1].toggle()
// 5. 切换右边的灯泡
if (col < cols - 1) bulbViews[row][col + 1].toggle()
}
}
算法思路(BFS 广度优先搜索)
重点在 Android 实现,但一个完整的解决方案离不开高效的算法,以下是 BFS 的核心思路:
-
状态表示:
- 整个网格的状态可以用一个一维或二维的整数数组(或 Bitmask)来表示。
0代表灭,1代表亮。 N x M的网格共有N*M个灯泡,所以总共有2^(N*M)种可能的状态。
- 整个网格的状态可以用一个一维或二维的整数数组(或 Bitmask)来表示。
-
BFS 队列:
- 队列中存放的不是简单的坐标,而是 “状态” 和 “到达该状态的步骤路径”。
- 我们可以用一个类
State来表示:data class State( val grid: Array<IntArray>, // 当前网格状态 val path: List<Pair<Int, Int>> // 到达此状态所经过的点击路径 )
-
BFS 流程:
- 初始化:创建一个初始状态(所有灯泡为灭),
path为空,将其加入队列,创建一个visited集合,记录已经访问过的状态,防止重复计算和死循环。 - 循环:当队列不为空时,取出队首的
State。- 检查这个
State.grid是否是目标状态(所有灯泡为亮)。 - 如果是,恭喜!
State.path就是我们需要的解,返回它。 - 如果不是,遍历网格中的每一个灯泡
(i, j),模拟一次“点击”。- 根据点击规则,生成一个新的网格状态
newGrid。 - 检查
newGrid是否在visited集合中,如果不在,说明这是一个新状态。 - 创建一个新的
State(newGrid, current.path + [(i, j)])。 - 将这个新
State加入队列,并标记newGrid为已访问。
- 根据点击规则,生成一个新的网格状态
- 检查这个
- 结束:如果队列为空仍未找到目标状态,说明该问题无解(对于
N x M的网格,通常都是有解的)。
- 初始化:创建一个初始状态(所有灯泡为灭),
注意:对于稍大的网格(如 5x5),状态空间 2^25 = 33,554,432 已经非常大,BFS 可能会非常慢或导致内存溢出,在实际面试或项目中,你可能需要讨论更优化的算法,如高斯消元法,它可以将问题转化为线性方程组求解,效率更高。
总结与面试要点
如果你在面试中被问到这个问题,你应该这样回答:
- 理解问题:清晰地复述题目,确认你理解了规则(点击自身和上下左右四个邻居)。
- 分析技术难点:
- 核心难点在于 Android 的 UI 线程模型,耗时计算不能放在主线程,否则会卡顿甚至 ANR。
- 需要使用多线程方案(如
Thread+Handler,或现代的Coroutines)来分离计算和 UI 更新。
- 提出解决方案架构:
- UI 层:使用
GridLayout或RecyclerView展示灯泡,每个灯泡是一个自定义View。 - 逻辑层:
- 后台线程:负责执行 BFS 算法,寻找解决方案。
- 主线程:负责初始化 UI 和接收后台线程传来的结果,然后逐步执行动画,更新每个灯泡的状态。
- UI 层:使用
- 阐述线程通信:
- 使用
lifecycleScope.launch(Dispatchers.IO)启动一个协程在后台计算。 - 计算完成后,使用
withContext(Dispatchers.Main)切换回主线程,安全地更新 UI。
- 使用
- 讨论算法优化:
- 提出使用 BFS 来寻找最短路径(最少步骤)。
- 可以提及对于大规模网格,BFS 可能效率不高,并引出更高级的算法如高斯消元法,展示你的算法广度。
- 代码实现:能够清晰地写出上述的 UI 布局、自定义 View 和使用 Coroutines 进行后台计算和 UI 更新的核心代码。 你可以充分展示你对 Android 开发核心概念的深刻理解,而不仅仅是会写算法。
标签: Android机器人控制灯方法 机器人点亮Android系统灯技巧 Android系统灯机器人控制实现