蓝桥杯 2024 年国赛题解(二)

May 24 · 12min

Tip

建议先刷题,再看题解😎!

新手引导

题目: 新手引导

这题的主要难度在于看懂题目和他给的代码。第一问要求我们借助他封装的函数 getDomWholeRect 来设置 introduce 的样式属性:

introduce.style.top = `${y}px`
if (isLeft) {
  introduce.style.left = `${right + distance}px`
}
else {
  introduce.style.left = `${left - getDomWholeRect(introduce).width - distance}px`
}

这里值得注意的是设置样式时要加上单位 px,否则样式不会生效。

第二问考察到了一个我之前没听过的方法:cloneNode,这个方法可以传入一个布尔值表示是否为深拷贝。第三问很简单只有一行代码:

function copyTarget() {
  //  TODO:待补充代码
  clone = target.cloneNode(true)
  clone.style.position = 'absolute'
  clone.style.width = `${boundingClientRect.width}px`
  clone.style.height = `${boundingClientRect.height}px`
  clone.style.left = `${boundingClientRect.x}px`
  clone.style.top = `${boundingClientRect.y}px`
  clone.style.zIndex = 999
  target.parentElement.append(clone)
  //  TODO:END
}

function removeTarget() {
  //  TODO:待补充代码
  clone?.remove()
  //  TODO:END
}

简易 webpack

题目: 简易 webpack

此题为 Node.js 代码题,职业组不考察。

第一问要求我们动态替换一个文件中的内容,并写入另一个文件夹中。先看代码:

// Task 1
let entryContent
        = fs.readFileSync(webpack.entry, { encoding: 'utf-8' })
for (let [src, tgt] of Object.entries(webpack.output.resolve.alias)) {
  entryContent = entryContent.replace(src, tgt)
}
// console.log(entryContent);
let outputPath = path.join(webpack.output.path, webpack.output.filename)
fs.writeFileSync(outputPath, entryContent)

这里的考点还是很多的:

  • readFileSync 传入 { encoding: 'utf-8' } 可以让文件内容以字符串的形式返回。
  • Object.entries 可以将对象转换为一个二维数组,每一项都是一个 [key, value] 的数组。
  • fs.writeFileSync 可以将字符串写入文件。
  • 注意 replace 是非就地操作,所以需要将结果重新赋值给 entryContent

第二问是一个难度更大的字符串替换:

// Task 2
let indexContent = fs.readFileSync(path.join(__dirname, 'index.html'), { encoding: 'utf-8' })
indexContent = indexContent.replace(/(?<=(src|href)=")(.+?)(?=")/g, (match) => {
  console.log(match)
  return `./${path.join(webpack.output.publicPath, match)}`
})
let indexOutputPath = path.join(webpack.output.path, 'index.html')
fs.writeFileSync(indexOutputPath, indexContent)
  • 我们在 replace 方法中使用了正则表达式和回调函数。正则表达式使用前瞻、后视断言来匹配路径,这样后面的 replacer 的第一个参数(完整匹配)就是路径。

  • 如果你不想用前后瞻断言,也可以用非捕获组:

    indexContent = indexContent.replace(/(?:src|href)="(.+?)"/g, (_, match) => {
      // 这里的 match 位于第二个参数的位置,是第一个捕获组,也就是路径。
    })
  • 注意我们匹配路径的时候始终使用的是 (.+?),其中的 ? 表示非贪婪匹配,这样可以匹配到最短的路径。

  • 注意我们要替换多处字符串,一定不要忘了给正则表达式加上 g 标志。

  • 实测如果写 'index.html' 而不是 path.join(__dirname, 'index.html') 会在云端评测时报错,不知道它内部评测的时候是什么环境,保险起见还是写一下。

第三问需要用递归或者 DFS / BFS 来遍历文件夹。这种做法我们会在后面的题目中展开讲;这道题有一个天才的做法:

require('node:child_process').execSync(`cp -r ${path.join(__dirname, 'static')} ${webpack.output.path}`)

借助命令行工具 cp 来复制文件夹,非常巧妙。(这种做法通常依赖评测机是 UNIX 系统,所以它只应该在你正常做法做不出来的时候才使用)

会议日程

题目: 会议日程

前两问都是非常简单的发送请求:

await axios.post('/api/meetings', params)
await axios.delete(`/api/delmeeting/${record.id}`)

你会发现之前的题目中没考察过用 fetch 发 POST 请求,主要是因为比较麻烦:

fetch('/api/meetings', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(params)
})

三四问需要缕清思路:

// 点击单个会议日程
function handleSelect(record) {
  record.checked = !record.checked
  const item = list.value.find(x => x.meetings.includes(record))
  item.meetings.every(({ checked }) => checked) ? handleSelectDate(item) : item.checked = false
}

// 点击日期多选框
function handleSelectDate(item) {
  item.checked = !item.checked
  item.meetings.forEach(x => x.checked = item.checked)
  allCheckStatus.value = list.value.every(({ checked }) => checked)
}

// 点击全选选择框
function handleSelectAll() {
  allCheckStatus.value = !allCheckStatus.value
  list.value.forEach((x) => {
    x.checked = allCheckStatus.value
    x.meetings.forEach(y => y.checked = allCheckStatus.value)
  })
}
  • 点击单个会议日程 -> 切换该会议的选中状态 -> 找到包含这个会议的日期项 item;如果 item 中的所有会议都选中了,那么选中 itemhandleSelectDate(item)),否则取消选中 item

  • 点击日期的多选框 -> 切换当天的选中状态 -> 选中/取消选中当天的所有会议 -> 同步更新全选框的选中状态。

  • 点击全选框 -> 切换全选框的选中状态 -> 选中/取消选中所有日期 -> 选中/取消选中每个日期下的所有会议。

代码量统计

题目: 代码量统计

这道题我们重点关注最后一问。我简化了题目,修改了一下最后一问的要求,运行这个测试代码:

function testStructDFS() {
  const tree = []

  // 模拟插入路径
  structDFS(tree, ['a', 'b', 'c'], 1)
  structDFS(tree, ['a', 'b', 'd'], 2)
  structDFS(tree, ['a', 'e'], 3)
  structDFS(tree, ['x', 'y'], 4)

  console.log(JSON.stringify(tree, null, 2))
}

预期得到的结果如下:

[
  {
    "name": "a",
    "children": [
      {
        "name": "b",
        "children": [
          {
            "name": "c",
            "children": [],
            "value": 1
          },
          {
            "name": "d",
            "children": [],
            "value": 2
          }
        ]
      },
      {
        "name": "e",
        "children": [],
        "value": 3
      }
    ]
  },
  {
    "name": "x",
    "children": [
      {
        "name": "y",
        "children": [],
        "value": 4
      }
    ]
  }
]

将路径转换为树形结构,并设置树形结构的最末级节点的 value 值。我们要想办法编写 structDFS 函数:

function structDFS(rootNodes, names, value) {
  const stack = [{ siblings: rootNodes, names }]

  while (stack.length) {
    const { siblings, names } = stack.pop()

    const curr = names.shift()

    let node = siblings.find(n => n.name === curr)

    if (!node) {
      node = { name: curr, children: [] }
      siblings.push(node)
    }

    if (names.length === 0) {
      node.value = value
    }
    else {
      stack.push({ siblings: node.children, names })
    }
  }
}

我们使用 深度优先 的遍历策略,维护一个栈,栈中元素是当前待处理的 names 和其同级节点的数组 siblings(我们要把新节点塞进这个数组里):

function structDFS(rootNodes, names, value) {
  const stack = [{ siblings: rootNodes, names }]

  while (stack.length) {
    // TODO
  }
}

每次从栈中取出一个元素,将其 names 的第一个元素取出,作为我们本次循环中要处理的 name

while (stack.length) {
  const { siblings, names } = stack.pop()
  const curr = names.shift()
}

当前同级列表 siblings 中,可能存在 curr,也可能不存在(例如我们只存入了[a, b, c], 现在有了新的[d, e, f],此时 siblings 中只有 a,没有 d)。对于不存在的情况,我们要手动构造一个节点并放入 siblings 中:

while (stack.length) {
  const { siblings, names } = stack.pop()
  const curr = names.shift()
  let node = siblings.find(n => n.name === curr)
  if (!node) {
    node = { name: curr, children: [] }
    siblings.push(node)
  }
}

现在我们已经构造好了节点并加入了 siblings(事实上这行是递归中实际的操作),接着只需要决定是否要继续递归即可:

  • 如果 names 中还有元素,说明当前节点不是最末级节点,需要继续递归;
  • 如果 names 中没有元素,说明当前节点是最末级节点,需要设置 value
while (stack.length) {
  const { siblings, names } = stack.pop()
  const curr = names.shift()
  let node = siblings.find(n => n.name === curr)
  if (!node) {
    node = { name: curr, children: [] }
    siblings.push(node)
  }
  if (names.length === 0) {
    node.value = value
  }
  else {
    stack.push({ siblings: node.children, names })
  }
}

事实上,DFS(维护一个栈)和 BFS(维护一个队列)仅在遍历树时的顺序不同,其余的逻辑是一样的。像在本题中,我们不关心遍历的顺序,所以可以非常简单地把 DFS 改成 BFS:

function structBFS(rootNodes, names, value) {
  const stack = [{ siblings: rootNodes, names }];
  const queue = [{ siblings: rootNodes, names }];
  while (stack.length) {
    const { siblings, names } = stack.pop();
    const { siblings, names } = queue.shift();
    const curr = names.shift();

    let node = siblings.find(n => n.name === curr);

    if (!node) {
      node = { name: curr, children: [] };
      siblings.push(node);
    }

    if (names.length === 0) {
      node.value = value;
    } else {
      stack.push({ siblings: node.children, names });
      queue.push({ siblings: node.children, names });
    }
  }
}

掌握这种思想,就可以解决很多题目了!

思考题:怎么把题目中的树形结构递归地转成如下这种结构:

[
  {
    "a": [
      {
        "b": [
          { "c": 1 },
          { "d": 2 }
        ]
      },
      { "e": 3 }
    ]
  },
  {
    "x": [
      { "y": 4 }
    ]
  }
]

>