<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>KaitoHub</title><description>Kaito的个人博客 - 分享技术与生活</description><link>https://kaitolab.net/</link><item><title>如何借助 AI 快速搭建个人技术博客</title><link>https://kaitolab.net/blog/build-blog-with-ai/</link><guid isPermaLink="true">https://kaitolab.net/blog/build-blog-with-ai/</guid><description>从零开始，借助 Claude Code 在两天内完成博客搭建、主题定制、内容迁移和部署上线的完整记录。</description><pubDate>Sun, 31 May 2026 16:00:00 GMT</pubDate><content:encoded>## 缘起

我一直想拥有一个真正属于自己的技术博客。之前用过 Hexo，虽然能跑，但主题老旧，定制困难，每次折腾都像在考古。2026 年，AI 辅助编程工具已经足够成熟，我决定试试——**能不能让 AI 帮我搞定一切？**

答案是：能，而且远超预期。

这篇文章记录了我借助 Claude Code 从零搭建本站的全过程，包括选型决策、功能实现、踩坑经验和最终成果。

## 技术选型

选框架时我给了 AI 几个约束：

1. **纯静态生成**，部署到 GitHub Pages 零成本
2. **Markdown 写作**，不依赖数据库和 CMS
3. **极致性能**，Lighthouse 满分是目标
4. **暗色模式**，夜间写作不伤眼

Claude 推荐了 [Astro](https://astro.build)，理由很充分：

- 零 JS 默认输出，生成的 HTML 只有几 KB
- 内置 Markdown/MDX 内容集合，类型安全
- Islands 架构，只在需要的地方加载交互 JS
- 社区模板丰富，`astro-paper` 起步模板非常干净

对比了 Next.js、Hugo、Hexo 之后，Astro 6 + `astro-paper` 模板胜出，一张白纸，高度可控。

## AI 辅助开发流水线

### 环境搭建（5 分钟）

```bash
npm create astro@latest -- --template satnaing/astro-paper
```

起步模板搭好后，Claude Code 就接管了。整个过程不需要手动写一行 CSS——描述需求，AI 生成代码，我 review，通过后提交。

### 主题系统（30 分钟）

第一件事是暗色模式。我们选择了 `data-theme` 属性方案：

- CSS 变量定义全部颜色，`:root` 放亮色，`[data-theme=&apos;dark&apos;]` 放暗色
- `&lt;head&gt;` 中同步执行脚本，读取 `localStorage`，**在首帧渲染前**注入 `data-theme`
- 支持「跟随系统」模式，`matchMedia` 监听系统主题变化

这里有一个重要教训：**FOUC 防闪烁脚本必须在 `&lt;head&gt;` 中同步执行**。如果放在 `defer` 或 `type=&quot;module&quot;` 的脚本里，暗色模式用户会先看到白色闪烁，体验极差。

### 首页重设计（1 小时）

原先的模板首页太简单——只有文章列表。我想要一个更丰富的首页：

- **左侧栏**：头像卡片 + 站点统计 + 友情链接
- **右侧栏**：最新 5 篇文章

站点统计是纯服务端计算的——从所有文章 body 中提取字符数、统计最早/最晚发布时间等。所有这些在 Astro 的构建阶段完成，生成纯静态 HTML，零客户端开销。

```typescript
// 构建时统计，零运行时开销
const stats = {
  articleCount: allPosts.length,
  tagCount: new Set(allPosts.flatMap(p =&gt; p.data.tags || [])).size,
  wordCount: allPosts.reduce((sum, p) =&gt; sum + countChars(p.body || &apos;&apos;), 0),
  runDays: Math.ceil((now - firstPost.data.pubDate) / 86400000),
};
```

### 博客列表时间轴（2 小时）

博客列表是最花时间的部分。我想要一个类似微信朋友圈的时间轴布局，而不是传统的列表。和 Claude Code 经过多轮迭代，最终确定了四栏 Grid 方案：

```
日期 | 圆点+连线 | 文章标题+标签 | 封面缩略图
100px |   28px   |     1fr      |   160px
```

细节打磨了很多轮：

- **年份分组**：同一年的文章只显示一次年份标题，用 Astro 模板中的 `let currentYear = 0` 变量追踪
- **圆点动画**：hover 时放大 1.5 倍 + accent 色光晕扩散，连线同步变色
- **标签筛选**：顶部的药丸按钮，点击只显示含该标签的文章，URL 参数 `?tag=xxx` 支持直达
- **翻页系统**：15 篇/页，智能页码 + 前后箭号，筛选和翻页联动，切换时保持滚动位置

### 文章详情页（1 小时）

每篇文章页面增加了多个功能：

- **阅读进度条**：顶部 sticky 定位，`requestAnimationFrame` 节流跟随滚动
- **文章目录 TOC**：右侧悬浮侧边栏，提取 h2~h4 标题，scroll 驱动高亮当前章节
- **上下篇导航**：底部两栏，显示相邻文章标题
- **社交分享**：复制链接 / X / 微博三个按钮
- **Giscus 评论**：基于 GitHub Discussions，零数据库

目录（TOC）的实现有一个关键细节——**标题 ID 的匹配**。Astro 的 Markdown 渲染器会自动给标题生成 ID，我们需要在服务端用同样的算法处理原始 Markdown 正文来提取标题文本和 ID，才能让 TOC 链接正确跳转。

```typescript
// 提取 h2-h4 标题并生成与渲染器一致的 ID
function extractHeadings(body: string) {
  const regex = /^(#{2,4})\s+(.+)$/gm;
  // ... 解析并生成 slug ID
}
```

### 关于页面时间轴（30 分钟）

关于页面用时间轴展示个人成长历程：教育经历 → 竞赛获奖 → 实习 → 工作。每条包含时间范围、持续时长、技能标签和成就高亮栏。

### AI 工具页面（15 分钟）

一个独立的工具推荐页面，卡片式展示我日常使用的 AI 工具（Claude、ChatGPT、Copilot 等），加上开源项目入口。

我还把这几个操作封装成了 Claude Code 的斜杠命令：

```markdown
# .claude/skills/ship.md
/ship → 一键 commit + push
/commit → git add + 生成 commit message + git commit
/push → git push origin &lt;分支名&gt;
```

省掉了每次手动输入 `git add` + `git commit` + `git push` 的重复劳动。

## AI 辅助编程的实战心得

### 1. 描述越具体，产出越精准

「把这个卡片美化一下」效果很差。有效的描述是：

&gt; 站点统计卡片改为 `&lt;table&gt;` 布局，每行左边是 SVG 图标（accent 色），中间是灰色标签，右边是加粗数值。行之间有细分隔线。

### 2. 善用「参考模板」

提供视觉参考——「参考下面模板，增加一个站点统计卡片」——远比抽象描述高效。AI 能准确理解你想要的布局和数据字段。

### 3. Review 不可省略

Claude Code 一次生成了整个文件的代码，直接替换。但以下几个坑是我自己发现的：

- Astro `.astro` 文件用 Tab 缩进，Edit 工具匹配空格会失败 → **Rule: 用 Write 重写整个文件**
- `is:inline` 脚本共享全局作用域，同名变量冲突 → **Rule: 用 IIFE 包裹**
- 暗色模式下卡片用 `#fff` 背景，切主题时不生效 → **Rule: 必须用 CSS 变量**

我把这些坑整理成了 `CLAUDE.md`，后续开发效率更高。

### 4. 迭代，不要一次到位

时间轴布局改了五版才满意——先是基础布局，然后加年份分组，再加动画，最后对齐导航栏。每一版都在前一版的基础上迭代，而不是推倒重来。

## 数据说话

| 指标 | 数值 |
|------|------|
| 开发耗时 | ~10 小时（含内容迁移） |
| AI 生成代码占比 | ~85% |
| 手动修改次数 | ~20 次 |
| 最终页面数 | 82 页（含 404） |
| Lighthouse 评分 | 100 / 100 |
| 首屏 JS | 0 KB |

## 技术栈总览

```
框架：     Astro 6
内容：     Markdown / MDX
样式：     手写 CSS（CSS 变量 + 主题系统）
评论：     Giscus（GitHub Discussions）
统计：     不蒜子
字体：     Atkinson Hyperlegible（本地托管）
部署：     GitHub Pages
AI 工具：  Claude Code（主力）+ ChatGPT / Copilot（辅助）
```

## 结语

两年前，搭一个博客要手写 HTML、调 CSS、配 Webpack、折腾部署。现在呢？打开 Claude Code，描述你要什么，AI 写出代码，你 review 通过，`/ship` 一键提交推送上线。

**AI 没有让编程变简单——它让「想法的实现成本」趋近于零。**

今天的 KaitoHub 可能还不够完美，但它的迭代速度会很快——因为每次改进只需要几分钟的对话。

如果你想用类似方式搭建自己的博客，可以参考 [Astro 官方文档](https://docs.astro.build) 和本站的 [GitHub 仓库](https://github.com/korolkk/astro-kaito)。欢迎 Fork、参考、交流。</content:encoded></item><item><title>Go 语言基础及实战案例｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note/</guid><description>Go语言开发环境搭建、基础语法与标准库使用入门｜字节跳动青训营笔记</description><pubDate>Sat, 14 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 一、本堂课重点内容：

### Go 语言学习背景介绍

### Go 语言基础语言详细讲解

1.  开发环境
2.  基础语法
3.  标准库

### Go 语言实战

1.  项目一：猜谜游戏
2.  项目二：命令行词典
3.  项目三：SOCKS5 代理

## 二、详细知识点介绍：

### Go 语言学习背景介绍

1.  高性能、高并发
2.  语法简单、学习曲线平缓
3.  丰富的标准库
4.  完善的工具链
5.  静态链接
6.  快速编译
7.  跨平台
8.  垃圾回收

### Go 语言基础语言详细讲解

*   开发环境

开发环境选择了VScode，安装了Golang，顺利运行样例程序Hello World

*   基础语法

1.  变量

```go
var a=&quot;initial&quot;
g:=a+&quot;apple&quot;
const s string = &quot;constant&quot;
const h = 500000
```

2.  数组

```go
var a[5]int
b:=[5]int{1,2,3,4,5}
var  towD [2][3]int
```

3.  切片

```go
s:=make([]string,3)
fmt.Println(s[2:5])
fmt.Println(s[:5])
fmt.Println(s[2:])
good:=[]string{&quot;g&quot;,&quot;o&quot;,&quot;o&quot;,&quot;d&quot;}
```

4.  map

```go
m:=make(map[string]int)
m[&quot;one&quot;]=1

r,ok:=m[&quot;unknow&quot;]
fmt.Println(r,ok) //0 false
```

5.  range

```go
nums:=[]int{2,3,4}
for i,num := range nums{
    if num==2{
        fmt.Println(&quot;index:&quot;,i,&quot;num:&quot;,num) //index:0 num:2
    }
}
m:=map[string]string{&quot;a&quot;:&quot;A&quot;,&quot;b&quot;:&quot;B&quot;}
for k,v :=range m{
    fmt.Println(k,v) //b B;a A
}
```

## 三、实践练习例子：

### 项目一：猜谜游戏

*   生成随机数

```go
rand.seed(time.Now().UnixNano())//很重要，需用时间戳来初始化随机数种子，否则生成伪随机数
secretNumber:=rand.Intn(maxNum)
```

*   读取用户输入

```go
reader := bufio.NewReader(os.Stdin)  //输入流
input, err := reader.ReadString(&apos;\n&apos;)
input = strings.Trim(input, &quot;\r\n&quot;)  //去除回车
guess, err := strconv.Atoi(input)  //字符转数字
```

*   实现游戏循环

```go
for {
    //···
}
```

*   实现判断逻辑

```go
if {
    //···
}else{
    //···
}
```

### 项目二：命令行词典

*   抓包

进入官网[彩云小译 - 在线翻译 (caiyunapp.com)](https://fanyi.caiyunapp.com/#/)

右键检查-Network-dict (请求 URL: [https://api.interpreter.caiyunai.com/v1/dict](https://api.interpreter.caiyunai.com/v1/dict) 请求方法: POST)

![uTools_1673778139893](bytedance-camp-note/uTools_1673778139893.png)

*   curl生成Go代码 [Convert curl to Go (curlconverter.com)](https://curlconverter.com/go/)

![uTools_1673778815476](bytedance-camp-note/uTools_1673778815476.png)

```go
package main

import (
    &quot;fmt&quot;
    &quot;io/ioutil&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;strings&quot;
)

func main() {
    client := &amp;http.Client{}
    var data = strings.NewReader(`{&quot;trans_type&quot;:&quot;en2zh&quot;,&quot;source&quot;:&quot;abandon&quot;}`)
    req, err := http.NewRequest(&quot;POST&quot;, &quot;https://api.interpreter.caiyunai.com/v1/dict&quot;, data)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set(&quot;authority&quot;, &quot;api.interpreter.caiyunai.com&quot;)
    req.Header.Set(&quot;accept&quot;, &quot;application/json, text/plain, */*&quot;)
    req.Header.Set(&quot;accept-language&quot;, &quot;zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6&quot;)
    req.Header.Set(&quot;app-name&quot;, &quot;xy&quot;)
    req.Header.Set(&quot;content-type&quot;, &quot;application/json;charset=UTF-8&quot;)
    req.Header.Set(&quot;device-id&quot;, &quot;&quot;)
    req.Header.Set(&quot;origin&quot;, &quot;https://fanyi.caiyunapp.com&quot;)
    req.Header.Set(&quot;os-type&quot;, &quot;web&quot;)
    req.Header.Set(&quot;os-version&quot;, &quot;&quot;)
    req.Header.Set(&quot;referer&quot;, &quot;https://fanyi.caiyunapp.com/&quot;)
    req.Header.Set(&quot;sec-ch-ua&quot;, `&quot;Not?A_Brand&quot;;v=&quot;8&quot;, &quot;Chromium&quot;;v=&quot;108&quot;, &quot;Microsoft Edge&quot;;v=&quot;108&quot;`)
    req.Header.Set(&quot;sec-ch-ua-mobile&quot;, &quot;?0&quot;)
    req.Header.Set(&quot;sec-ch-ua-platform&quot;, `&quot;Windows&quot;`)
    req.Header.Set(&quot;sec-fetch-dest&quot;, &quot;empty&quot;)
    req.Header.Set(&quot;sec-fetch-mode&quot;, &quot;cors&quot;)
    req.Header.Set(&quot;sec-fetch-site&quot;, &quot;cross-site&quot;)
    req.Header.Set(&quot;user-agent&quot;, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76&quot;)
    req.Header.Set(&quot;x-authorization&quot;, &quot;token:qgemv4jr1y38jyq6vhvi&quot;)
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    bodyText, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf(&quot;%s\n&quot;, bodyText)
}
```

*   生成request body

```go
type DictRequest struct {
    TransType string `json:&quot;trans_type&quot;`
    Source    string `json:&quot;source&quot;`
    UserID    string `json:&quot;user_id&quot;`
}
```

*   解析response body[JSON转Golang Struct - 在线工具 - OKTools](https://oktools.net/json2go)

```go
type DictResponse struct {
    Rc   int `json:&quot;rc&quot;`
    Wiki struct {
        KnownInLaguages int `json:&quot;known_in_laguages&quot;`
        Description     struct {
            Source string      `json:&quot;source&quot;`
            Target interface{} `json:&quot;target&quot;`
        } `json:&quot;description&quot;`
        ID   string `json:&quot;id&quot;`
        Item struct {
            Source string `json:&quot;source&quot;`
            Target string `json:&quot;target&quot;`
        } `json:&quot;item&quot;`
        ImageURL  string `json:&quot;image_url&quot;`
        IsSubject string `json:&quot;is_subject&quot;`
        Sitelink  string `json:&quot;sitelink&quot;`
    } `json:&quot;wiki&quot;`
    Dictionary struct {
        Prons struct {
            EnUs string `json:&quot;en-us&quot;`
            En   string `json:&quot;en&quot;`
        } `json:&quot;prons&quot;`
        Explanations []string      `json:&quot;explanations&quot;`
        Synonym      []string      `json:&quot;synonym&quot;`
        Antonym      []string      `json:&quot;antonym&quot;`
        WqxExample   [][]string    `json:&quot;wqx_example&quot;`
        Entry        string        `json:&quot;entry&quot;`
        Type         string        `json:&quot;type&quot;`
        Related      []interface{} `json:&quot;related&quot;`
        Source       string        `json:&quot;source&quot;`
    } `json:&quot;dictionary&quot;`
}
```

[完整代码](https://github.com/wangkechun/go-by-example/tree/master/simpledict/v4)

### 项目三：SOCKS5 代理

*   原理

![uTools_1673779907033](bytedance-camp-note/uTools_1673779907033.png)

*   author部分

```
    // +----+----------+----------+
    // VER  NMETHODS  METHODS  
    // +----+----------+----------+
    //  1      1      1 to 255 
    // +----+----------+----------+
    // VER: 协议版本，socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS，NMETHODS的值为多少，METHODS就有多少个字节。RFC预定义了一些值的含义，内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD
```

*   Requests部分

```
    // +----+-----+-------+------+----------+----------+
    // VER  CMD   RSV   ATYP  DST.ADDR  DST.PORT 
    // +----+-----+-------+------+----------+----------+
    //  1    1   X&apos;00&apos;   1    Variable     2     
    // +----+-----+-------+------+----------+----------+
    // VER 版本号，socks5的值为0x05
    // CMD 0x01表示CONNECT请求
    // RSV 保留字段，值为0x00
    // ATYP 目标地址类型，DST.ADDR的数据对应这个字段的类型。
    //   0x01表示IPv4地址，DST.ADDR为4个字节
    //   0x03表示域名，DST.ADDR是一个可变长度的域名
    // DST.ADDR 一个可变长度的值
    // DST.PORT 目标端口，固定2个字节
```

*   Replies部分

```
    // +----+-----+-------+------+----------+----------+
    // VER  REP   RSV   ATYP  BND.ADDR  BND.PORT 
    // +----+-----+-------+------+----------+----------+
    //  1    1   X&apos;00&apos;   1    Variable     2     
    // +----+-----+-------+------+----------+----------+
    // VER socks版本，这里为0x05
    // REP Relay field,内容取值如下 X’00’ succeeded
    // RSV 保留字段
    // ATYPE 地址类型
    // BND.ADDR 服务绑定的地址
    // BND.PORT 服务绑定的端口DST.PORT
```

[完整代码](https://github.com/wangkechun/go-by-example/tree/master/proxy/v4)

## 四、课后作业：

### 修改第一个例子猜谜游戏里面的最终代码，使用fmt.Scanf来简化代码实现

```go
package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;
)

func main() {
    maxNum := 100
    rand.Seed(time.Now().UnixNano())
    secretNumber := rand.Intn(maxNum)
    // fmt.Println(&quot;The secret number is &quot;, secretNumber)

    fmt.Println(&quot;Please input your guess&quot;)
    for {
        var guess int
        fmt.Scanf(&quot;%d &quot;, &amp;guess)
        fmt.Println(&quot;You guess is&quot;, guess)
        if guess &gt; secretNumber {
            fmt.Println(&quot;Your guess is bigger than the secret number. Please try again&quot;)
        } else if guess &lt; secretNumber {
            fmt.Println(&quot;Your guess is smaller than the secret number. Please try again&quot;)
        } else {
            fmt.Println(&quot;Correct, you Legend!&quot;)
            break
        }
    }
}
```

### 修改第二个例子命令行词典里面的最终代码，增加另一种翻译引擎的支持

```go
//go run .\go-by-example\simpledict\v4\main.go hello
package main

import (
    &quot;bytes&quot;
    &quot;encoding/json&quot;
    &quot;fmt&quot;
    &quot;io/ioutil&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;os&quot;
)

type DictRequest struct {
    TransType string `json:&quot;trans_type&quot;`
    Source    string `json:&quot;source&quot;`
    UserID    string `json:&quot;user_id&quot;`
}

type DictResponse struct {
    Rc   int `json:&quot;rc&quot;`
    Wiki struct {
        KnownInLaguages int `json:&quot;known_in_laguages&quot;`
        Description     struct {
            Source string      `json:&quot;source&quot;`
            Target interface{} `json:&quot;target&quot;`
        } `json:&quot;description&quot;`
        ID   string `json:&quot;id&quot;`
        Item struct {
            Source string `json:&quot;source&quot;`
            Target string `json:&quot;target&quot;`
        } `json:&quot;item&quot;`
        ImageURL  string `json:&quot;image_url&quot;`
        IsSubject string `json:&quot;is_subject&quot;`
        Sitelink  string `json:&quot;sitelink&quot;`
    } `json:&quot;wiki&quot;`
    Dictionary struct {
        Prons struct {
            EnUs string `json:&quot;en-us&quot;`
            En   string `json:&quot;en&quot;`
        } `json:&quot;prons&quot;`
        Explanations []string      `json:&quot;explanations&quot;`
        Synonym      []string      `json:&quot;synonym&quot;`
        Antonym      []string      `json:&quot;antonym&quot;`
        WqxExample   [][]string    `json:&quot;wqx_example&quot;`
        Entry        string        `json:&quot;entry&quot;`
        Type         string        `json:&quot;type&quot;`
        Related      []interface{} `json:&quot;related&quot;`
        Source       string        `json:&quot;source&quot;`
    } `json:&quot;dictionary&quot;`
}

type DictRequest2 struct {
    Source         string   `json:&quot;source&quot;`
    Words          []string `json:&quot;words&quot;`
    SourceLanguage string   `json:&quot;source_language&quot;`
    TargetLanguage string   `json:&quot;target_language&quot;`
}

type DictResponse2 struct {
    Details []struct {
        Detail string `json:&quot;detail&quot;`
        Extra  string `json:&quot;extra&quot;`
    } `json:&quot;details&quot;`
    BaseResp struct {
        StatusCode    int    `json:&quot;status_code&quot;`
        StatusMessage string `json:&quot;status_message&quot;`
    } `json:&quot;base_resp&quot;`
}

type Detail struct {
    ErrorCode string `json:&quot;errorCode&quot;`
    RequestID string `json:&quot;requestId&quot;`
    Msg       string `json:&quot;msg&quot;`
    Result    []struct {
        Ec struct {
            ReturnPhrase []string `json:&quot;returnPhrase&quot;`
            Web          []struct {
                Phrase   string   `json:&quot;phrase&quot;`
                Meanings []string `json:&quot;meanings&quot;`
            } `json:&quot;web&quot;`
            Synonyms []struct {
                Pos   string   `json:&quot;pos&quot;`
                Words []string `json:&quot;words&quot;`
                Trans string   `json:&quot;trans&quot;`
            } `json:&quot;synonyms&quot;`
            Etymology struct {
                ZhCHS []struct {
                    Description string `json:&quot;description&quot;`
                    Detail      string `json:&quot;detail&quot;`
                    Source      string `json:&quot;source&quot;`
                } `json:&quot;zh-CHS&quot;`
            } `json:&quot;etymology&quot;`
            MTerminalDict string `json:&quot;mTerminalDict&quot;`
            Dict          string `json:&quot;dict&quot;`
            Basic         struct {
                UsPhonetic string   `json:&quot;usPhonetic&quot;`
                UsSpeech   string   `json:&quot;usSpeech&quot;`
                Phonetic   string   `json:&quot;phonetic&quot;`
                UkSpeech   string   `json:&quot;ukSpeech&quot;`
                ExamType   []string `json:&quot;examType&quot;`
                Explains   []struct {
                    Pos   string `json:&quot;pos&quot;`
                    Trans string `json:&quot;trans&quot;`
                } `json:&quot;explains&quot;`
                UkPhonetic  string `json:&quot;ukPhonetic&quot;`
                WordFormats []struct {
                    Name  string `json:&quot;name&quot;`
                    Value string `json:&quot;value&quot;`
                } `json:&quot;wordFormats&quot;`
            } `json:&quot;basic&quot;`
            Phrases []struct {
                Phrase   string   `json:&quot;phrase&quot;`
                Meanings []string `json:&quot;meanings&quot;`
            } `json:&quot;phrases&quot;`
            Lang           string `json:&quot;lang&quot;`
            SentenceSample []struct {
                Sentence     string `json:&quot;sentence&quot;`
                SentenceBold string `json:&quot;sentenceBold&quot;`
                Translation  string `json:&quot;translation&quot;`
            } `json:&quot;sentenceSample&quot;`
            IsWord  bool   `json:&quot;isWord&quot;`
            WebDict string `json:&quot;webDict&quot;`
        } `json:&quot;ec&quot;`
    } `json:&quot;result&quot;`
}

func query(word string) {
    client := &amp;http.Client{}
    request := DictRequest{TransType: &quot;en2zh&quot;, Source: word}
    buf, err := json.Marshal(request)
    if err != nil {
        log.Fatal(err)
    }
    var data = bytes.NewReader(buf)
    req, err := http.NewRequest(&quot;POST&quot;, &quot;https://api.interpreter.caiyunai.com/v1/dict&quot;, data)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set(&quot;Connection&quot;, &quot;keep-alive&quot;)
    req.Header.Set(&quot;DNT&quot;, &quot;1&quot;)
    req.Header.Set(&quot;os-version&quot;, &quot;&quot;)
    req.Header.Set(&quot;sec-ch-ua-mobile&quot;, &quot;?0&quot;)
    req.Header.Set(&quot;User-Agent&quot;, &quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36&quot;)
    req.Header.Set(&quot;app-name&quot;, &quot;xy&quot;)
    req.Header.Set(&quot;Content-Type&quot;, &quot;application/json;charset=UTF-8&quot;)
    req.Header.Set(&quot;Accept&quot;, &quot;application/json, text/plain, */*&quot;)
    req.Header.Set(&quot;device-id&quot;, &quot;&quot;)
    req.Header.Set(&quot;os-type&quot;, &quot;web&quot;)
    req.Header.Set(&quot;X-Authorization&quot;, &quot;token:qgemv4jr1y38jyq6vhvi&quot;)
    req.Header.Set(&quot;Origin&quot;, &quot;https://fanyi.caiyunapp.com&quot;)
    req.Header.Set(&quot;Sec-Fetch-Site&quot;, &quot;cross-site&quot;)
    req.Header.Set(&quot;Sec-Fetch-Mode&quot;, &quot;cors&quot;)
    req.Header.Set(&quot;Sec-Fetch-Dest&quot;, &quot;empty&quot;)
    req.Header.Set(&quot;Referer&quot;, &quot;https://fanyi.caiyunapp.com/&quot;)
    req.Header.Set(&quot;Accept-Language&quot;, &quot;zh-CN,zh;q=0.9&quot;)
    req.Header.Set(&quot;Cookie&quot;, &quot;_ym_uid=16456948721020430059; _ym_d=1645694872&quot;)
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    bodyText, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    if resp.StatusCode != 200 {
        log.Fatal(&quot;bad StatusCode:&quot;, resp.StatusCode, &quot;body&quot;, string(bodyText))
    }
    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &amp;dictResponse)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(word, &quot;UK:&quot;, dictResponse.Dictionary.Prons.En, &quot;US:&quot;, dictResponse.Dictionary.Prons.EnUs)
    for _, item := range dictResponse.Dictionary.Explanations {
        fmt.Println(item)
    }
    fmt.Println(&quot;By CaiYun&quot;)
}

func query2(word string) {
    client := &amp;http.Client{}
    request := DictRequest2{Source: &quot;youdao&quot;, Words: []string{word}, SourceLanguage: &quot;en&quot;, TargetLanguage: &quot;zh&quot;}
    buf, err := json.Marshal(request)
    if err != nil {
        log.Fatal(err)
    }
    var data = bytes.NewReader(buf)
    req, err := http.NewRequest(&quot;POST&quot;, &quot;https://translate.volcengine.com/web/dict/detail/v1/?msToken=&amp;X-Bogus=DFSzswVLQDadxLyJSZW76tteJnyE&amp;_signature=_02B4Z6wo00001ljRzmgAAIDCU11wL0iWDHZY0crAAPYG0kiMNFxedoitm5HLGUD0jZ5DcXIetnNY8ZGiALMbzgBDv1KOARzAc9zVX0hBbkTuGlfrQXxOs7iKnzDbkKrVsENB0LBFxbQpr7cx6d&quot;, data)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set(&quot;authority&quot;, &quot;translate.volcengine.com&quot;)
    req.Header.Set(&quot;accept&quot;, &quot;application/json, text/plain, */*&quot;)
    req.Header.Set(&quot;accept-language&quot;, &quot;zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6&quot;)
    req.Header.Set(&quot;content-type&quot;, &quot;application/json&quot;)
    req.Header.Set(&quot;cookie&quot;, &quot;x-jupiter-uuid=16737825730907524; i18next=zh-CN; s_v_web_id=verify_lcxaxfl8_82b7bTsx_spXo_4ojy_8eRx_vYijmz0YGEDK; ttcid=53e7468bfdd8463d9035633a3619cd1e27; tt_scid=kFfR0z7TRSKbDMjd5nuEbx.LtFvHzHFpDi1yQWzIA0zpggD470g9zU3L.lxerpe6c31c&quot;)
    req.Header.Set(&quot;origin&quot;, &quot;https://translate.volcengine.com&quot;)
    req.Header.Set(&quot;referer&quot;, &quot;https://translate.volcengine.com/mobile?category=&amp;home_language=zh&amp;source_language=detect&amp;target_language=zh&amp;text=hello&quot;)
    req.Header.Set(&quot;sec-ch-ua&quot;, `&quot;Not?A_Brand&quot;;v=&quot;8&quot;, &quot;Chromium&quot;;v=&quot;108&quot;, &quot;Microsoft Edge&quot;;v=&quot;108&quot;`)
    req.Header.Set(&quot;sec-ch-ua-mobile&quot;, &quot;?0&quot;)
    req.Header.Set(&quot;sec-ch-ua-platform&quot;, `&quot;Windows&quot;`)
    req.Header.Set(&quot;sec-fetch-dest&quot;, &quot;empty&quot;)
    req.Header.Set(&quot;sec-fetch-mode&quot;, &quot;cors&quot;)
    req.Header.Set(&quot;sec-fetch-site&quot;, &quot;same-origin&quot;)
    req.Header.Set(&quot;user-agent&quot;, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76&quot;)
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    bodyText, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    var dictResponse2 DictResponse2
    var detail Detail
    err = json.Unmarshal(bodyText, &amp;dictResponse2)
    if err != nil {
        log.Fatal(err)
    }
    err = json.Unmarshal([]byte(dictResponse2.Details[0].Detail), &amp;detail)
    if err != nil {
        log.Fatal(err)
    }
    //fmt.Printf(&quot;%v&quot;, detail)
    fmt.Println(word, &quot;UK:&quot;, detail.Result[0].Ec.Basic.Phonetic, &quot;US:&quot;, detail.Result[0].Ec.Basic.UsPhonetic)
    for _, item := range detail.Result[0].Ec.Basic.Explains {
        fmt.Println(item.Pos, item.Trans)
    }
    fmt.Println(&quot;By HuoShan&quot;)
}

func main() {
    start := time.Now()
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
        `)
        os.Exit(1)
    }
    word := os.Args[1]
    query(word)
    fmt.Println()
    query2(word)
    elapsed := time.Since(start)
    fmt.Println(elapsed)
}
```

### 在上一步骤的基础上，修改代码实现并行请求两个翻译引擎来提高响应速度

修改main函数通过goroutine实现并发

```go
func main() {
    start := time.Now()
    if len(os.Args) &lt; 2 {
        fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
        `)
        os.Exit(1)
    }
    words := os.Args[1:]
    var word string
    for i, item := range words {
        if i != len(words)-1 {
            word += item + &quot; &quot;
        } else {
            word += item
        }
    }
    go func() {
        query(word)
    }()
    query2(word)
    time.Sleep(time.Second)
    elapsed := time.Since(start)
    fmt.Println(elapsed)
}
```

响应速度对比可得，并发后响应速度提升明显

![uTools_1673798979137](bytedance-camp-note/uTools_1673798979137.png)</content:encoded></item><item><title>【实践课】手把手教你做系统设计 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-10/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-10/</guid><description>系统设计的概念是什么    如何做系统设计        4S分析法（场景、存储、服务、扩展）    如何分析系统瓶颈和优化        火焰图分析        链路分析        全链路压测    如何验证系统的可用性和稳定...</description><pubDate>Mon, 06 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 系统设计方法论

*   系统设计的概念是什么
*   如何做系统设计
    *   4S分析法（场景、存储、服务、扩展）
*   如何分析系统瓶颈和优化
    *   火焰图分析
    *   链路分析
    *   全链路压测
*   如何验证系统的可用性和稳定性
    *   链路梳理
    *   可观测性
    *   全链路测试
    *   稳定性控制
    *   容灾演练

## 电商秒杀业务介绍

### 基本概念

*   Spu(Standard Product Unit)

商品信息聚合的最小单位，是一组可复用、易检索的标准化信息的集合，该集合描述了一个产品的特性。

*   Sku(Stock Keeping Unit)

物理上不可分割的最小存货单元。

### 秒杀的特点

*   瞬时流量高
*   读多写少
*   实时性要求高

### 秒杀的挑战

*   资源有限性
*   反欺诈
*   高性能
*   防止超卖
*   流量管控
*   扩展性
*   鲁棒性

### 设计流程

#### 4S分析

*   场景分析

秒杀场景读多写少，可使用Redis作为缓存层，并且进行缓存预热

并发量大，不能直接调用扣减库存、生成订单等API，可以加入MQ进行流量削峰

*   存储设计

Localcache、Redis、MySQL

将系统细分为如下模块：秒杀服务、商品服务、订单服务和支付服务

*   服务设计

基础组件：ID生成器、缓存组件、MQ组件、限流组件

库存扣减应该加锁实现

借用Redis来实现分布式事务（seata，ZK都可以实现）

*   可扩展性

流量隔离、流量管控、CDN、缓存优化、数据库扩展、Redis扩展、服务水平垂直扩展

#### 系统架构图

![uTools_1675697827238](bytedance-camp-note-10/uTools_1675697827238.png)

## 课程实践

### 秒杀流程

![uTools_1675697941158](bytedance-camp-note-10/uTools_1675697941158.png)

## 课后总结

今天课程是实践课，课上学习了如何实现一个秒杀系统。之前的课程中一直学习的是Go语言，但这次实践课使用的却是java，不过从系统设计的角度出发，语言并不是最关键的因素。

**高性能系统的通用设计思想**非常重要，秒杀系统的扩展方面还有很大的空间</content:encoded></item><item><title>黑灰产监控与防御 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-11/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-11/</guid><description>企业的信息安全体系是非常庞大的，任何一个环节都可能会出现安全风险。其中，黑灰产是安全人员最为关注的一个风险来源，也是历年来导致企业和用户损失最大的因素。</description><pubDate>Mon, 06 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 一、概述

企业的信息安全体系是非常庞大的，任何一个环节都可能会出现安全风险。其中，黑灰产是安全人员最为关注的一个风险来源，也是历年来导致企业和用户损失最大的因素。

如果某个平台或者业务被黑灰产盯上，可能是因为这个业务存在安全隐患被黑灰产利用，也可能只是被黑灰产当做牟利的垫脚石。对黑灰产的监控和防御，就是要了解他们的意图、手段和行为模式，避免被黑灰产攻击或者利用。

本次可能会给大家简单介绍国内黑灰产的情况，挑选了几种比较经典的黑产作弊手段进行详细分析，希望能帮助大家对黑灰产这个群体有一定的了解，提升各位的安全意识，在日后的工作和生活中，多一些安全角度的思考。

## 二、国内黑产介绍

### 常见黑产

诈骗、木马、薅羊毛、钓鱼、黄牛、病毒、博彩、拖库、引流、盗号、跑分、勒索软件

### 发展趋势

规模化

*   借助脚本、软件实现攻击的批量化
*   上游各类资源丰富，大大降低攻击成本，同时攻击成功率也比较高

组织化

*   以工作室形式
*   分工明确、合作紧密
*   某些黑产甚至成立了公司

平台化

*   平台级的爬虫、群控、钓鱼、DDoS攻击工具，攻击成本大大降低且难以追责
*   各类平台将黑产手中零散的资源进行整合

## 　三、常见的黑惨技术分析

*   人脸识别对抗
*   地理位置对抗
*   银行业务逻辑漏洞

## 四、安全防护体系的建设

### 事前

*   情报监控
    *   暗网
    *   贴吧
    *   TG
    *   破解论坛
*   SDLC
*   漏洞扫描

### 事中

*   渗透测试
*   威胁感知
    *   用户行为异常
    *   接口数据异常
    *   恶意流量检测
*   风控/安全策略

### 事后

*   威胁建模
*   攻击溯源

### 其他

*   验证码
*   安全SDK
*   代理检测
*   人脸识别
*   行为审计

## 五、思考

*   身边是否有一些事情是可能与黑产有关的，如何辨别？
*   当前所学习和研究的技术，是否存在一些公开的安全问题，比如漏洞或者设计缺陷？如何避免他人利用这些问题来攻击你？
*   如果无法避免被攻击，如何将损失降低到最小？</content:encoded></item><item><title>分布式定时任务 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-12/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-12/</guid><description>业务流程             定时扫描抖音用户集卡状态        汇总计算用户的瓜分金额        定时开奖    技术体量             亿级用户规模        十亿级资金规模        百万级读写QP...</description><pubDate>Tue, 07 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 一、前言

*   业务流程
    
    *   定时扫描抖音用户集卡状态
    *   汇总计算用户的瓜分金额
    *   定时开奖
*   技术体量
    
    *   亿级用户规模
    *   十亿级资金规模
    *   百万级读写QPS
*   方案引出
    
    *   自动化 + 定时执行 + 海量数据 + 高效稳定 = 分布式定时任务

## 二、发展历程

### 发展历史

*   Windows批处理
    
*   Windows任务计划程序
    
*   Linux命令-CronJob
    

![e9bc7cae-8ada-457d-8332-01522341cf15](bytedance-camp-note-12/e9bc7cae-8ada-457d-8332-01522341cf15.png)

*   单机定时任务-Timer、Ticker
    
*   单机定时任务-ScheduledExecutorService
    

![aa6e926e-ff42-4ed6-9ded-7e2dfc2d1db8](bytedance-camp-note-12/aa6e926e-ff42-4ed6-9ded-7e2dfc2d1db8.png)

*   任务调度- Quartz

![44a76c7c-5045-4957-a5c9-27f23390c20d](bytedance-camp-note-12/44a76c7c-5045-4957-a5c9-27f23390c20d.png)

*   分布式定时任务

![下载](bytedance-camp-note-12/下载.png)

### 分布式定时任务

#### 定义

*   定时任务是指系统为了**自动**完成特定任务，**实时、延时、周期**性完成任务调度的过程。
*   分布式定时任务是把分散的、可靠性差的定时任务纳入统一的**平台**，并实现集群管理调度和**分布式部署**的一种定时任务的管理方式。

#### 特点

*   自动化、平台化、分布式、伸缩性、高可用
    
*   执行方式
    
    *   单机任务
    *   广播任务
    *   Map任务
    *   MapReduce任务

![6a5b73f1-3a9e-461f-95c4-0d518269009a](bytedance-camp-note-12/6a5b73f1-3a9e-461f-95c4-0d518269009a.png)

**发奖金额计算：MapReduce 定时开奖：Map**

#### 业内定时任务框架

*   大众点评Xxl-job
    
    Xxl-job很大一个优势在于开源且免费，并且轻量级，开箱即用，操作简易，上手快，企业维护起来成本不高，因而在中小型公司使用非常广泛。
    
*   阿里巴巴SchedulerX
    
    分布式任务调度SchedulerX2.0是阿里巴巴基于Akka架构自研的新一代分布式任务调度平台，提供定时调度、调度任务编排和分布式批量处理等功能。 SchedulerX可在阿里云付费使用。它功能非常强大，在阿里巴巴内部广泛使用并久经考验。
    
*   腾讯TCT
    
    仅在内部使用，未开源、未商用
    

## 三、实现原理

### 整体架构

*   核心架构

![img](bytedance-camp-note-12/62fdceaada184704b53f995b04bfa6a1tplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp)

*   数据流

![下载 (1)](bytedance-camp-note-12/下载 (1).png)

*   功能架构

![c73571bc-6b2f-468d-b30b-285ec655ed01](bytedance-camp-note-12/c73571bc-6b2f-468d-b30b-285ec655ed01.png)

### 控制台Admin

*   任务：Job，任务元数据
*   任务实例：JobInstance,周期任务会生 成多个任务实例
*   任务结果：JobResult,任务实例运行的 结果
*   任务历史：JobHistory,用户可以修改任 务信息，任务实例对应的任务元数据可 以不同，因而使用任务历史存储

### 触发器Trigger

*   定期扫描+ 延时消息（腾讯、字节方案）

![1f44bdfc-1f7b-4444-8dd8-9b9e600dcd3d](bytedance-camp-note-12/1f44bdfc-1f7b-4444-8dd8-9b9e600dcd3d.png)

*   时间轮（Quartz方案）
    
    时间轮是一种高效利用线程资源进行批量化调度的一种调度模型。时间轮是一个存储环形队列，底层采用数组实现，数组中的每个元素可以存放一个定时任务列表。
    

![b2e7c91a-2ac8-4f2a-885d-8bfb00dd1317](bytedance-camp-note-12/b2e7c91a-2ac8-4f2a-885d-8bfb00dd1317.png)

*   高可用
    *   存储：不同国别、业务做资源隔离
    *   运行：不同级别、业务分开执行
    *   部署：采用多机房集群化部署，避免单点故障，通过数据库锁或分布式锁保证只被触发一次

调度器Scheduler

#### 资源来源

*   业务系统
    *   优点：任务执行逻辑与业务系统共用一份资源，利用率更高
    *   缺点：容易发生影响在线服务的事故，不能扩缩容
*   定时任务平台
    *   优点：任务执行逻辑与业务系统相互隔离，优雅地扩缩容
    *   缺点：消耗更多机器资源，需要额外为定时任务平台申请接口调用权限

#### 资源调度

*   节点选择
    *   随机节点执行：选择集群中一个可用的执行节点执行调度任务。适用场景：定时对账。
    *   广播执行：在集群中所有的执行节点分发调度任务并执行。适用场景：批运维。
    *   分片执行：按照用户自定义分片逻辑进行拆分，分发到集群中不同节点并行执行，提升资源利用效率。适用场景：海星日志统计。
*   任务分片
    *   通过任务分片来提高任务执行的效率和资源地利用率
*   故障转移
    *   分片任务基于一致性hash策略分发任务，当某Executor异常时，调度器会将任务分发到其他Executor，任务最终成功

#### 执行器Executor

![1a9fc62a-132c-4e3f-8141-0c8691379b6e](bytedance-camp-note-12/1a9fc62a-132c-4e3f-8141-0c8691379b6e.png)

## 四、业务应用

所有需要定时、延时、周期性执行任务的业务场景，都可以考虑使用分布式定时任务

*   电商
    *   订单30分钟未付款自动关闭订单
    *   定时给商家、达人发送消品，给·的奖励用户发放优惠券等
*   互动
    *   支付宝集五福
    *   字节吞节集卡瓜分红包
*   游戏
    *   活动估束后批量补发用户未领取
    *   定期更新游戏内榜单

## 五、课程收获

*   知识面扩充
    *   对分布式定时任务建立起宏观的认知，并深入了解其实现原理
    *   了解关联的单机定时任务、大数据处理引擎，通过了解不同实现方案的优劣来拓展知识面
*   项目实践能力加强
    *   了解在哪些实际业务场景中使用分布式定时任务
    *   对于实际业务场景的中间件选型、技术方案设计做到成竹在胸</content:encoded></item><item><title>JWT认证 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-14/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-14/</guid><description>参数</description><pubDate>Thu, 09 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 配置

参数

介绍

`Key`

用于设置签名密钥（**必要配置**）

`KeyFunc`

用于设置获取签名密钥的回调函数，设置后 token 解析时将从 `KeyFunc` 获取 `jwt` 签名密钥

`Timeout`

用于设置 token 过期时间，默认为一小时

`MaxRefresh`

用于设置最大 token 刷新时间，允许客户端在 `TokenTime` + `MaxRefresh` 内刷新 token 的有效时间，追加一个 `Timeout` 的时长

`Authenticator`

用于设置登录时认证用户信息的函数（**必要配置**）

`Authorizator`

用于设置授权已认证的用户路由访问权限的函数

`PayloadFunc`

用于设置登陆成功后为向 token 中添加自定义负载信息的函数

`Unauthorized`

用于设置 jwt 验证流程失败的响应函数

## 流程

### 用户登陆（认证）

注册完成后，服务器需要在用户第一次登陆的时候，验证用户账号和密码，并签发 jwt token。

```go
JwtMiddleware, err = jwt.New(&amp;jwt.HertzJWTMiddleware{
    Key:           []byte(&quot;secret key&quot;),
    Timeout:       time.Hour,
    MaxRefresh:    time.Hour,
    Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
        var loginStruct struct {
            Account  string `form:&quot;account&quot; json:&quot;account&quot; query:&quot;account&quot; vd:&quot;(len($) &gt; 0 &amp;&amp; len($) &lt; 30); msg:&apos;Illegal format&apos;&quot;`
            Password string `form:&quot;password&quot; json:&quot;password&quot; query:&quot;password&quot; vd:&quot;(len($) &gt; 0 &amp;&amp; len($) &lt; 30); msg:&apos;Illegal format&apos;&quot;`
        }
        if err := c.BindAndValidate(&amp;loginStruct); err != nil {
            return nil, err
        }
        users, err := mysql.CheckUser(loginStruct.Account, utils2.MD5(loginStruct.Password))
        if err != nil {
            return nil, err
        }
        if len(users) == 0 {
            return nil, errors.New(&quot;user already exists or wrong password&quot;)
        }

        return users[0], nil
    },
    PayloadFunc: func(data interface{}) jwt.MapClaims {
        if v, ok := data.(*model.User); ok {
            return jwt.MapClaims{
                jwt.IdentityKey: v,
            }
        }
        return jwt.MapClaims{}
    },
})
```

*   Authenticator：用于设置登录时认证用户信息的函数，demo 当中定义了一个 `loginStruct` 结构接收用户登陆信息，并进行认证有效性。这个函数的返回值 `users[0]` 将为后续生成 jwt token 提供 payload 数据源。
    
*   PayloadFunc：它的入参就是 `Authenticator` 的返回值，此时负责解析 `users[0]`，并将用户名注入 token 的 payload 部分。
    
*   Key：指定了用于加密 jwt token 的密钥为 `&quot;secret key&quot;`。
    
*   Timeout：指定了 token 有效期为一个小时。
    
*   MaxRefresh：用于设置最大 token 刷新时间，允许客户端在 `TokenTime` + `MaxRefresh` 内刷新 token 的有效时间，追加一个 `Timeout` 的时长。
    

### Token 的返回

```verilog
JwtMiddleware, err = jwt.New(&amp;jwt.HertzJWTMiddleware{
    LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
        c.JSON(http.StatusOK, utils.H{
            &quot;code&quot;:    code,
            &quot;token&quot;:   token,
            &quot;expire&quot;:  expire.Format(time.RFC3339),
            &quot;message&quot;: &quot;success&quot;,
        })
    },
})
```

*   LoginResponse：在登陆成功之后，jwt token 信息会随响应返回，你可以自定义这部分的具体内容，但注意不要改动函数签名，因为它与 `LoginHandler` 是强绑定的。

### Token 的校验

访问配置了 jwt 中间件的路由时，会经过 jwt token 的校验流程。

```go
JwtMiddleware, err = jwt.New(&amp;jwt.HertzJWTMiddleware{
    TokenLookup:   &quot;header: Authorization, query: token, cookie: jwt&quot;,
    TokenHeadName: &quot;Bearer&quot;,
    HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
        hlog.CtxErrorf(ctx, &quot;jwt biz err = %+v&quot;, e.Error())
        return e.Error()
    },
    Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
        c.JSON(http.StatusOK, utils.H{
            &quot;code&quot;:    code,
            &quot;message&quot;: message,
        })
    },
})
```

*   TokenLookup：用于设置 token 的获取源，可以选择 `header`、`query`、`cookie`、`param`，默认为 `header:Authorization`，同时存在是以左侧一个读取到的优先。当前 demo 将以 `header` 为数据源，因此在访问 `/ping` 接口时，需要你将 token 信息存放在 HTTP Header 当中。
    
*   TokenHeadName：用于设置从 header 中获取 token 时的前缀，默认为 `&quot;Bearer&quot;`。
    
*   HTTPStatusMessageFunc：用于设置 jwt 校验流程发生错误时响应所包含的错误信息，你可以自行包装这些内容。
    
*   Unauthorized：用于设置 jwt 验证流程失败的响应函数，当前 demo 返回了错误码和错误信息。
    

### 用户信息的提取

```go
JwtMiddleware, err = jwt.New(&amp;jwt.HertzJWTMiddleware{
    IdentityKey: IdentityKey,
    IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
        claims := jwt.ExtractClaims(ctx, c)
        return &amp;model.User{
            UserName: claims[IdentityKey].(string),
        }
    },
})

// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
    user, _ := c.Get(mw.IdentityKey)
    c.JSON(200, utils.H{
        &quot;message&quot;: fmt.Sprintf(&quot;username:%v&quot;, user.(*model.User).UserName),
    })
}
```

*   IdentityHandler：用于设置获取身份信息的函数，在 demo 中，此处提取 token 的负载，并配合 `IdentityKey` 将用户名存入上下文信息。
*   IdentityKey：用于设置检索身份的键，默认为 `&quot;identity&quot;`。
*   Ping：构造响应结果，从上下文信息中取出用户名信息并返回。

### 参数绑定

hertz 使用开源库 [go-tagexpr](https://github.com/bytedance/go-tagexpr) 进行参数的绑定及验证，demo 中也频繁使用了这个特性。

```csharp
var loginStruct struct {
    // 通过声明 tag 进行参数绑定和验证
    Account  string `form:&quot;account&quot; json:&quot;account&quot; query:&quot;account&quot; vd:&quot;(len($) &gt; 0 &amp;&amp; len($) &lt; 30); msg:&apos;Illegal format&apos;&quot;`
    Password string `form:&quot;password&quot; json:&quot;password&quot; query:&quot;password&quot; vd:&quot;(len($) &gt; 0 &amp;&amp; len($) &lt; 30); msg:&apos;Illegal format&apos;&quot;`
}
if err := c.BindAndValidate(&amp;loginStruct); err != nil {
    return nil, err
}
```</content:encoded></item><item><title>消息队列与RPC原理 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-15/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-15/</guid><description>消息队列中间件是分布式系统中重要的组件，主要解决应用解耦，异步消息，流量削锋等问题，实现高性能，高可用，可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ，RabbitMQ，ZeroMQ，Kafka，MetaMQ，Roc...</description><pubDate>Sat, 11 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 消息队列

消息队列中间件是[分布式系统](https://so.csdn.net/so/search?q=分布式系统&amp;spm=1001.2101.3001.7020)中重要的组件，主要解决应用解耦，异步消息，流量削锋等问题，实现高性能，高可用，可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ，RabbitMQ，ZeroMQ，Kafka，MetaMQ，RocketMQ

### 消息队列是什么

*   解耦

![解耦](bytedance-camp-note-15/c5be8cc7291f46598766c8f510426ac3.png)

订单系统：用户下单后，订单系统完成持久化处理，将消息写入消息队列，返回用户订单下单成功

库存系统：订阅下单的消息，采用拉/推的方式，获取下单信息，库存系统根据下单信息，进行库存操作

假如：在下单时库存系统不能正常使用。也不影响正常下单，因为下单后，订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

*   削峰

流量削锋也是消息队列中的常用场景，一般在秒杀或团抢活动中使用广泛。

应用场景：秒杀活动，一般会因为流量过大，导致流量暴增，应用挂掉。为解决这个问题，一般需要在应用前端加入消息队列。

a、可以控制活动的人数

b、可以缓解短时间内高流量压垮应用

用户的请求，服务器接收后，首先写入消息队列。假如消息队列长度超过最大数量，则直接抛弃用户请求或跳转到错误页面。

秒杀业务根据消息队列中的请求信息，再做后续处理

*   异步

![watermark,type_ZmFuZ3poZW5naGVpdGk](bytedance-camp-note-15/watermark,type_ZmFuZ3poZW5naGVpdGk.png)

按照以上约定，用户的响应时间相当于是注册信息写入数据库的时间，也就是50毫秒。注册邮件，发送短信写入消息队列后，直接返回，因此写入消息队列的速度很快，基本可以忽略，因此用户的响应时间可能是50毫秒。因此架构改变后，系统的吞吐量提高到每秒20 QPS。比串行提高了3倍，比并行提高了两倍。

*   日志处理

日志处理是指将消息队列用在日志处理中，比如[Kafka](https://so.csdn.net/so/search?q=Kafka&amp;spm=1001.2101.3001.7020)的应用，解决大量日志传输的问题。架构简化如下

 ![日志处理](bytedance-camp-note-15/ed51387339de4828a73cd377e43f1130.png) 

日志采集客户端，负责日志数据采集，定时写受写入Kafka队列

Kafka消息队列，负责日志数据的接收，存储和转发

日志处理应用：订阅并消费kafka队列中的日志数据

### 消息队列的前世今生

#### 消息队列-Kafka

kafka使用场景，业务日志、用户行为数据、Metrics数据

基本概念，Producer、Cluster、Consumer、Topic、Partition

数据迁移、Offset、Partition选主

一条消息从生产到消费是如何处理的，Producer端逻辑、Broker端逻辑、Consumer端逻辑

#### 消息队列-BMQ

Kafka在使用中遇到问题

BMQ架构

BMQ各模块是如何工作的，Broker、Proxy、HDFS、MetaStorage

BMQ多机房容灾

## RPC 原理与实践

### RPC 的基本概念

RPC（Remote Procedure Call Protocol）远程过程调用协议。一个通俗的描述是：客户端在不知道调用细节的情况下，调用存在于远程计算机上的某个对象，就像调用本地应用程序中的对象一样。比较正式的描述是：一种通过网络从远程计算机程序上请求服务，而不需要了解底层网络技术的协议。

*   Client：RPC协议的调用方。就像上文所描述的那样，最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用。但实际情况来说Client或多或少的都需要指定RPC框架的一些细节。
*   Server：在RPC规范中，这个Server并不是提供RPC服务器IP、端口监听的模块。而是远程服务方法的具体实现（在JAVA中就是RPC服务接口的具体实现）。其中的代码是最普通的和业务相关的代码，甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用。
*   Stub/Proxy：RPC代理存在于客户端，因为要实现客户端对RPC框架“透明”调用，那么客户端不可能自行去管理消息格式、不可能自己去管理网络传输协议，也不可能自己去判断调用过程是否有异常。这一切工作在客户端都是交给RPC框架中的“代理”层来处理的。
*   Message Protocol：在上文我们已经说到，一次完整的client-server的交互肯定是携带某种两端都能识别的，共同约定的消息格式。RPC的消息管理层专门对网络传输所承载的消息信息进行编号和解码操作。**目前流行的技术趋势是不同的RPC实现，为了加强自身框架的效率都有一套（或者几套）私有的消息格式。**例如前文所讲到的RMI框架使用的消息协议为JRMP；后文我们将详细讲解的RPC框架Thrift也有私有的消息协议，“- Transfer/Network Protocol”（当然它还支持一些通用的消息格式，如JSON）。
*   Transfer/Network Protocol：传输协议层负责管理RPC框架所使用的网络协议、网络IO模型。例如Hessian的传输协议基于HTTP（应用层协议）；而Thrift的传输协议基于TCP（传输层协议）。传输层还需要统一RPC客户端和RPC服务端所使用的IO模型；
*   Selector/Processor：存在于RPC服务端，由于服务器端某一个RPC接口的实现的特性（它并不知道自己是一个将要被RPC提供给第三方系统调用的服务）。所以在RPC框架中应该有一种“负责执行RPC接口实现”的角色。它负责了包括：管理RPC接口的注册、判断客户端的请求权限、控制接口实现类的执行在内的各种工作。
*   IDL：实际上IDL（接口定义语言）并不是RPC实现中所必须的。但是需要跨语言的RPC框架一定会有IDL部分的存在。这是因为要找到一个各种语言能够理解的消息结构、接口定义的描述形式。如果您的RPC实现没有考虑跨语言性，那么IDL部分就不需要包括，例如JAVA RMI因为就是为了在JAVA语言间进行使用，所以JAVA RMI就没有相应的IDL。

### RPC 框架分层设计

![RPC 框架分层设计](bytedance-camp-note-15/6d12fff7fde5429b814f15c64fc4a261tplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp)

#### 编解码层

*   数据格式
    
    *   语言特定格式：例如 java.io.Serializable
    *   文本格式：例如 JSON、XML、CSV 等
    *   二进制编码：常见有 Thrift 的 BinaryProtocol，Protobuf，实现可以有多种形式，例如 TLV 编码 和 Varint 编码
*   选型考察点
    
    *   兼容性
    *   通用型
    *   [性能](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Falecthomas%2Fgo_serialization_benchmarks)
    *   空间开销
    *   时间开销
*   生成代码和编解码层相互依赖，框架的编解码应当具备扩展任意编解码协议的能力
    

![编解码层](bytedance-camp-note-15/7ffa6428b4214c6590a3b8a6fc106390tplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp)

#### 协议层

*   以 Thrift 的 THeader 协议为例

![协议层](bytedance-camp-note-15/228aa11d265240c3aad35736194de94ftplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp)

*   协议解析

![协议解析](bytedance-camp-note-15/ae3b22f299dc4e4a8df34793263055cbtplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp)

#### 网络通信层

![网络通信层](bytedance-camp-note-15/c4e3c7f3fb234b6f9f1102fca16818ddtplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp)

*   阻塞 IO 下，耗费一个线程去阻塞在 read(fd) 去等待用足够多的数据可读并返回。
    
*   非阻塞 IO 下，不停对所有 fds 轮询 read(fd) ，如果读取到 n &lt;= 0 则下一个循环继续轮询。
    

第一种方式浪费线程（会占用内存和上下文切换开销），第二种方式浪费 CPU 做大量无效工作。而基于 IO 多路复用系统调用实现的 Poll 的意义在于将可读/可写状态通知和实际文件操作分开，并支持多个文件描述符通过一个系统调用监听以提升性能。 网络库的核心功能就是去同时监听大量的文件描述符的状态变化(通过操作系统调用)，并对于不同状态变更，高效，安全地进行对应的文件操作。</content:encoded></item><item><title>大项目Redis的原理及应用 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-16/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-16/</guid><description>Redis核心原理与实战应用，深入理解数据结构、持久化机制与缓存策略</description><pubDate>Tue, 21 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## Redis介绍

### 简单概念

*   redis作为NoSql的一种，拥有NoSQL相关特点
    
*   redis支持数据的持久化，将内存中的数据保存到磁盘中，重启的时候可以再次加载进行使用
    
*   redis不仅仅支持key-value数据，还支持：string,list,set,hash等
    
*   redis支持数据备份，即master-slaver模式的数据备份
    

### 常见命令

通用指令是部分数据类型的，都可以使用的指令，常见的有:

*   `KEYS`:查看符合模板的所有key，不建议在生产环境设备上使用
*   `DEL`:删除一个指定的key
*   `EXISTS`:判断key是否存在
*   `EXPIRE`:给一个key设置有效期，有效期到期时该key会被自动删除
*   `TTL`:查看一个KEY的剩余有效期 (-1：代表永久有效)

还有其他几种数据结构操作命令参考[Redis](https://link.juejin.cn?target=https%3A%2F%2Fredis.io%2F)官网

## go操作Redis

### 获取Redis连接

```go
func GetRedisConnection() redis.Conn {
    c, err := redis.Dial(&quot;tcp&quot;, &quot;localhost:6379&quot;)
    if err != nil {
        fmt.Println(&quot;conn redis failed,&quot;, err)
        return nil
    }
    fmt.Println(&quot;redis conn success&quot;)

    return c
}
```

`Dial`获取Redis连接：本地Redis使用6379端口

### 操作基础命令

*   Set命令

```go
//获取redis连接
    conn := utils.GetRedisConnection()
    if conn == nil {
        fmt.Println(&quot;获取redis连接失败&quot;)
    }

    //将token存入用户信息存入redis中
    token := utils.RandString(32)
    userJson, err := json.Marshal(user)
    if err != nil {
        fmt.Println(err)
    }
//使用set命令
    conn.Do(&quot;set&quot;, constant.USER_FLAG+token, userJson)
```

*   Get命令

```go
conn := utils.GetRedisConnection()
    userjson, err := redis.String(conn.Do(&quot;get&quot;, constant.USER_FLAG+token))
    if err != nil {
        fmt.Println(err)
    }

    var user common.User
    json.Unmarshal([]byte(userjson), &amp;user)
    //redis中不存在用户信息，从数据库中查找
    if user.Username == &quot;&quot; {
        user = dao.GetInfos(userId)
    }
```

注：`redis.string()方法`将`get`命令返回的数据转成字符串，还有类似的`redis.int()方法`

### 基本思路

每个用户有都有一个关注 set 和粉丝 set，set 的存储的是用户的唯一 ID。

每当用户关注某人时，就会被写入到关注 set 中，同时对方的粉丝 set 也会记录当前用户

当需要判断两人是否进行互相关注时，可以直接使用 redis 中的 集合操作，找出对方关注 set 的交集。

原本我们使用 SQL 来实现这个功能的，性能上并不是很高效。

这样就可以免去使用 MySQL 查询实现了，更高效地提高性能。

### 操作Set相关命令

在上述操作Redis基础命令上，我们进阶一下使用最常用的set数据结构命令

*   判断用户是否是成员

```go
//省略Redis连接
//redis中set判断是否存在
        result, err := redis.Int(conn.Do(&quot;SISMEMBER&quot;, constant.VIDEO_FLAG+string(videoId), user.UserId))
        //不存在，点赞操作
        if err != nil {
            fmt.Println(err)
        }
复制代码
```

当`result==1`时:表示是存在该成员变量

当`result==0`时:表示是不存在该成员变量

*   获取所有成员

```go
conn.Do(&quot;SMEMBERS&quot;, constant.VIDEO_FLAG+string(videoId))
```

返回SET集合该key的所有成员

### 注意点

使用 redis 作为缓存需要额外注意“缓存失效”的情况。当 redis 查不到数据时，需要转而查询 MySQL 后端数据库，并且查到数据后，一定要将其更新到 redis 服务器中！

这里有一个提升效率的小细节：在往 redis 服务器更新数据的时候，可以作异步处理，另开一个协程进行更新。

这样当从 MySQL 查询出数据后，不必等待 redis 服务器的更新就可以及时将数据响应了！</content:encoded></item><item><title>Go-Hertz Web框架实现简易抖音 ｜ 字节跳动青训营后端结营项目</title><link>https://kaitolab.net/blog/bytedance-camp-note-17/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-17/</guid><description>项目核心信息：简易版抖音后端服务    Github 地址：https://github.com/qiong-14/EasyDouYin</description><pubDate>Thu, 23 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 一、项目介绍

&gt; 项目核心信息：简易版抖音后端服务
&gt; 
&gt; Github 地址：[https://github.com/qiong-14/EasyDouYin](https://github.com/qiong-14/EasyDouYin)

## 二、项目分工

| **团队成员** | **主要贡献**                                                 |
| ------------ | ------------------------------------------------------------ |
| 万禹         | 负责项目文件结构管理、用户接口、视频点赞、喜欢列表的接口设计以及对应数据库设计 |
| 韩冰         | 负责视频Feed流、用户视频上传以及对应数据库设计，minio对象储存服务，redis设计 |
| 范乃丹       | 负责用户关注操作，查询用户关注列表的接口设计以及对应数据库设计 |
| 聂嘉璐       | 负责查询用户粉丝列表，用户好友列表接口设计以及对应数据库设计 |
| 徐凯凯       | 负责JWT鉴权，redis存储userId，消息操作、聊天记录接口，Demo视频录制 |
| 袁博艺       | 负责评论功能和评论列表接口的设计与实现                       |

## 三、项目实现

### 3.1 技术选型与相关开发文档

#### 3.1.1 Web开发套件

*   **[Hertz](https://github.com/cloudwego/hertz)**

现有的web开发的HTTP框架大多使用gin框架，而如今越来越多的web服务转向微服务架构，Hertz作为一个字节跳动内部的微服务HTTP框架，在设计之初参考了其他开源框架的优势，并结合字节跳动内部的需求，使其具有高易用性、高性能、高扩展性等特点，对微服务性能有着很好的提升。同时，Hertz支持包括[Autotls](https://github.com/hertz-contrib/autotls), [Http2](https://github.com/hertz-contrib/http2)等各种扩展，更加利于用户的不同定制化的需求。

*   **[GORM](https://github.com/go-gorm/gorm)**

gorm框架作为golnag语言一款性能极好的ORM库，支持多种数据库。使用起来简单方便，支持链式操作和原生SQL，对开发人员相对友好。另外，现阶段gorm框架有良好的文档和社区支持，非常方便学习和解决问题。基于以上优点，我们选择gorm作为我们的ORM框架。

#### 3.1.2 数据库选型

关系型数据库是基于关系模型的数据库，它使用二维表来存储结构化的数据。关系型数据库适合存储需要做结构化查询、事务性、一致性的数据，比如用户的账号、地址等。剩余的数据，比如非结构化或半结构化的数据，比如图片、视频、文档等，不能存在关系型中，因为它们不适合用二维表来表示和查询。这些数据可以使用非关系型数据库来存储，比如HBase、Cassandra、Redis等。在本项目中，有关用户关系、视频信息（不包括视频文件本身）的内容都可以用关系型数据库来储存，而视频文件就需要文件系统辅助存储。因此我们选用Mysql来存储用户信息、视频信息、用户视频关系、用户关注关系、聊天信息结构化的数据，并用Redis对部分数据进行缓存。

#### 3.1.3 对象存储服务的应用

视频的元信息需要持久化存储到关系型数据库, 包括视频的标题、封面、所有者、视频的标签以及当前的状态(待审核、已审核待上线、待修改、已删除)。

视频的封面和视频本身属于超大的不定规模的二进制数据, 可以存到文件系统中, 以FTP或基于FTP的方式向外访问, 但这种方式有很多弊端, 比如视频链接可能会长期有效, 给服务器造成很大负担, 如果视频被删除, 需要同步删除视频本身以避免视频泄露, 以及潜在的FTP安全漏洞.

基于以上考量, 这样的二进制数据存储到对象存储服务, 如MinIO中. 经过了解, Minio可以产生仅在一段时间内有效的URL传递到前端. [MinIO官方](https://www.minio.org.cn/)也有[Go的SDK](https://github.com/minio/minio-go)方便开发者使用.

#### 3.1.4 Redis对MySQL慢查询的补充

### 3.2 架构设计

![img](bytedance-camp-note-17/16772272045911.png)

### 3.3 项目代码介绍

### 

| 目录            | 文件        | 说明                                                     |
| --------------- | ----------- | -------------------------------------------------------- |
| biz/handler/    |             | 路由处理函数                                             |
| 基础接口        | feed.go     | **视频流接口**                                           |
|                 | user.go     | **用户注册**&lt;br /&gt;**用户登录**&lt;br /&gt;**用户信息**         |
|                 | publish.go  | **视频投稿**&lt;br /&gt;**发布列表**                           |
| 互动接口        | comment.go  | **评论操作**&lt;br /&gt;**评论列表**                           |
|                 | favorite.go | **赞操作**&lt;br /&gt;**喜欢列表**                             |
| 社交接口        | message.go  | **聊天记录**&lt;br /&gt;**消息操作**                           |
|                 | relation.go | **关系操作**&lt;br /&gt;**用户关注列表**&lt;br /&gt;**用户好友列表** |
| biz/resp/       | common.go   | 响应结构体定义                                           |
| biz/router      | register.go | 路由注册                                                 |
| configs/sql     | init.sql    | 数据库初始化语句                                         |
| constants/      | constant.go | 项目中定义的常量内容                                     |
| dal/            |             | 数据库相关内容                                           |
|                 | init.go     | 数据库初始化                                             |
|                 | comment.go  | 评论表 comment                                           |
|                 | like.go     | 点赞表 like_video                                        |
|                 | message.go  | 消息表 message                                           |
|                 | relation.go | 社交表 follow                                            |
|                 | user.go     | 用户表 user                                              |
|                 | video.go    | 视频表 video                                             |
| middleware/jwt/ | jwt.go      | jwt鉴权                                                  |
|                 | minio.go    | minio对象储存服务                                        |
|                 | redis.go    | redis缓存                                                |
| service/        | user.go     | 带缓存的用户操作接口                                     |
|                 | video.go    | 带缓存的视频操作接口                                     |
| tools/          | common.go   | 项目公用函数                                             |
| ./              | main.go     | 项目入口                                                 |

### 3.4 数据库表设计

![img](bytedance-camp-note-17/16772272300534.png)

以上为数据库表的关系图，根据功能设计我们设计了六张表，红色为主表，其他表基于这两张表来拓展功能：



|       表名        |            用途            |  关联的表  |                        主要键及其作用                        |
| :---------------: | :------------------------: | :--------: | :----------------------------------------------------------: |
|     **user**      |      储存用户名和密码      |            |        name 用户名（账号）password 用户密码（加密后）        |
|     **video**     |      储存视频相关信息      |            | owner_id 视频作者idtitle 视频标题label 视频标签likes_count 点赞人数 |
|  **like_video**   |  储存用户对视频的点赞信息  | user video |             user_id 用户idvideo_id 点赞的视频id              |
| **comment_video** |  储存用户对视频的评论信息  | user video |             user_id 用户idvideo_id 评论的视频id              |
|    **message**    | 储存用户之间发送的消息信息 |    user    | from_user_id 发送消息用户idto_user_id 接受消息用户idcontent 消息内容 |
|    **follow**     |   储存用户之间的关注信息   |    user    |          followed_id 被关注用户idfollow_id 关注者id          |

### 3.5 用户名验证和密码强度评估

对于用户注册时输入的用户名和密码我们可以在后端进行进行验证，如果用户名不符合特定格式不允许用户进行注册（本项目使用的是邮箱格式，只有符合邮箱格式的用户名才能通过注册），以及对于密码强度较弱的用户拒绝注册。采用正则表达式匹配用户名和密码字符串实现以上功能。

| 分数         | 0      | 2                   | 3                            | 5                               | 10         | 15   | 20        | 25   |
| ------------ | ------ | ------------------- | ---------------------------- | ------------------------------- | ---------- | ---- | --------- | ---- |
| 密码长度     | &lt;8     |                     |                              | 5~8                             |            | 8~10 |           | &gt;=10 |
| 英文大小写   | 无字母 |                     |                              |                                 | 大写或小写 |      | 大写+小写 |      |
| 数字个数     | 无数字 |                     |                              |                                 | 1~3        |      | &gt;=3       |      |
| 特殊字符个数 |        |                     |                              |                                 | 1          |      |           | &gt;1   |
| 奖励分       |        | 数字+字母大写或小写 | 数字+特殊字符+字母大写或小写 | 数字+特殊字符+字母大写+字母小写 |            |      |           |      |

## 四、测试结果

**通过demo的测试函数进行功能测试：**

![img](bytedance-camp-note-17/16772272349127.png)

## 五、项目总结与反思

&gt; 1.  目前仍存在的问题
&gt; 2.  已识别出的优化项
&gt; 3.  架构演进的可能性
&gt; 4.  项目过程中的反思与总结

从刚开始的完全不了解go语言到项目的完成，我们组遇到过不少的困难，但是最终还是坚持完成了项目。总结来说，本项目完整的实现所有的服务功能，由于时间和阅历的问题，项目也存在些许不足之处：

1.  微服务架构方面，采用了Hertz架构为以后扩展微服务进行了准备，当前业务还是耦合性较强，不利于业务细化。
2.  对于高并发场景：数据库方面只使用了redis缓存进行优化，没有进行分库分表，分区操作，读写分离等，服务器方面没有采用消息队列，负载均衡等方法</content:encoded></item><item><title>Go 语言入门-工程实践｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-2/</guid><description>本节课程主要分为四个方面：</description><pubDate>Sun, 15 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 一、本堂课重点内容：

本节课程主要分为四个方面：

*   语言进阶
    
*   依赖管理
    
*   单元测试
    
*   项目实战
    

## 二、详细知识点介绍：

### 语言进阶

#### Goroutine

协程：用户态，轻量级线程，栈KB级别

线程：内核态，线程跑多个协程，栈MB级别

*   CSP(Communicating Sequential Processes)

提倡**通过通信共享内存**而不是通过共享内存而实现通信

#### Channel

make(chan 元素类型,\[缓存大小\])

无缓冲通道 make(chan int)

有缓存通道make(chan int,2) 生产·消费问题

#### 并发安全 Lock

var lock sync.Mutex

通过lock.Lock()和lock.Unlock()实现协程的互斥操作，达到并发安全

#### WaitGroup

计数器：开启协程+1；执行结束-1；主协程阻塞直到计数器为0

```go
//快速打印hello goroutine：0-4
func hello(i int) {
    println(&quot;hello world : &quot; + fmt.Sprint(i))
}
//不使用WaiGroup
func HelloGoRoutine() {
    for i := 0; i &lt; 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}
//使用WaiGroup更优雅
func ManyGoWait() {
    var wg sync.WaitGroup
    wg.Add(delta:5)
    for i := 0; i &lt; 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}
```

### 依赖管理

#### 背景

工程项目不可能基于标准库0-1编码搭建，更加注重逻辑层面编码，因此要依赖管理

#### Go依赖管理演进

**GOPATH-&gt;Go Vendor-&gt;Go Module**

不同环境（项目）依赖的版本不同，要控制依赖库的版本

1.环境变量$GOPATH

```
$GOPATH
├── bin //项目编译的二进制文件
├── pkg //项目编译的中间产物，加速编译
└── src //项目源码
```

弊端：若场景A和B依赖于某一package的不同版本，无法实现package的多版本控制

项目代码直接依赖src下的代码

go get下载最新版本的包到src目录下

2.GoVender

弊端：无法控制依赖的版本。更新项目又可能出现依赖冲突，导致编译出错。

3.GoModule

可以定义规则管理关系。

#### Go Module实践

依赖管理三要素

1.  配置文件，描述依赖 go.mod

version:

语义化版本`${MAJOR}.${MINOR}.${PATCH}`

基于commit伪版本`vX.0.O-yyyymmddhhmmss-abcdefgh1234`(时间戳+哈希码)

2.  中心仓库管理依赖库 Proxy
    
3.  本地工具 go get/mod
    

go mod:

init 初始化，创建go.mod文件 download 下载模块到本地缓存 tidy 增加需要的依赖，删除不需要的依赖

### 单元测试

#### 规则：

*   所有测试文件以\_test.go 结尾
*   func TestXxx(\*testing.T)
*   初始化逻辑放到TestMain中

可以增加--cover参数来显示覆盖率

#### Mock

常用 monkey : [github.com/bouk/monkey](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fbouk%2Fmonkey)

#### 基准测试

基准测试用于对代码进行性能分析。

以Benchmark开头，Resttimer重置计时器，runparallel是多协程并发测试。

在并发情况下rand为了保证并发安全和随机性有了全局锁，性能下降，可以使用fastrand提升性能。

## 三、实践练习例子-青训营话题页开发：

### 需求描述

*   展示话题（标题，文字描述）和回帖列表
*   暂不考虑前端页面实现，仅仅实现一个本地web服务
*   话题和回帖数据用文件存储

### ER图

1个话题可以对应多个帖子，但一个帖子只能属于一个话题。

![uTools_1673940532987](bytedance-camp-note-2/uTools_1673940532987-169070656623024.png)

### 分层结构

![uTools_1673940807668](bytedance-camp-note-2/uTools_1673940807668-169070659514827.png)

数据层：数据Model，外部数据的增删改查

逻辑层：业务Entity，处理核心业务逻辑输出

视图层：视图View，处理和外部的交互逻辑

### 组件工具

Gin高性能go web框架

[https://github.com/gin-gonic/gin#installation](https://github.com/gin-gonic/gin#installation)

Go mod

go mod init

go get gopkg.in/gin-gonic/gin.v1@v1.3.0

### Repository

```json
// Topic
{
    &quot;id&quot;:1,
    &quot;title&quot;:&quot;青训营来啦！&quot;,
    &quot;content&quot;:&quot;欢迎踊跃报名！&quot;,
    &quot;create_time&quot;:100213123123
}

// Post
{
    &quot;id&quot;:1,
    &quot;parent_id&quot;:1,
    &quot;content&quot;:&quot;欢迎欢迎！&quot;,
    &quot;create_time&quot;:2131312
}
```

以下代码中用到了sync.once，主要适用高并发的场景下只执行一次的场景，这里的基于once的实现模式就是我们平常说的单例模式，减少存储的浪费。

```go
var topicDao *TopicDao
var topicOnce sync.Once

func NewTopicDaoInstance() *TopicDao {
    topicOnce.Do(
        func() {
            topicDao = &amp;TopicDao{}
        })
    return topicDao
}
```

### Service

首先定义service实体

```go
type PageInfo struct {
    TopicInfo *TopicInfo
    PostList  []*PostInfo
}
```

代码流程编排

```mermaid
graph LR
    参数校验 --&gt; 准备数据 --&gt; 组装实体
```

话题信息和回帖信息都依赖topicid，可以并行查询，提高执行效率。一定要多思考是否可以并行，通过压榨CPU，降低接口耗时，不要一味的串行实现，浪费多核CPU的资源。

```go
func (f *QueryPageInfoFlow) prepareInfo() error {
    //获取topic信息
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        defer wg.Done()
        //
    }()
    //获取post列表
    go func() {
        defer wg.Done()
        //
    }()
    wg.Wait()
    return nil
}
```

### View

下面就是controller层。这里我们定义一个view对象，通过code msg 打包业务状态信息，用data承载业务实体信息

```go
type PageData struct {
    Code int64       `json:&quot;code&quot;`
    Msg  string      `json:&quot;msg&quot;`
    Data interface{} `json:&quot;data&quot;`
}
```

### 运行测试

成功得到返回结果（中文乱码，问题不大。。

![uTools_1673956939847](bytedance-camp-note-2/uTools_1673956939847.png)

## 四、课后个人总结：

本次课程知识点较多，需要慢慢消化吸收。

仅仅是跟着老师实现一遍，还是遇到很多的问题，但是开发流程有了一定了解。

还有课后实践没有完成：

*   支持发布帖子。
*   本地id生成需要保证不重复、唯一性。
*   Append文件，更新索引，注意Map的并发安全问题。</content:encoded></item><item><title>Go 高质量编程与性能调优 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-3/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-3/</guid><description>Go语言高质量编程规范与性能调优实战，涵盖边界条件、异常处理与代码可维护性</description><pubDate>Tue, 17 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 一、高质量编程：

### 高质量编程简介

#### 高质量代码

*   各种边界条件考虑完备
*   异常情况处理，稳定性保证
*   易读易维护

#### 编程原则

*   简单性：简单清晰的逻辑
*   可读性：确保代码可读
*   生产力：团队整体工作效率非常重要

### 编码规范

#### 代码格式

推荐自动格式化代码

#### 注释

**公共符号始终要注释**

*   解释代码作用
*   解释代码如何做的
*   解释代码实现的原因
*   解释代码什么情况会出错

#### 命名规范

*   注意变量的作用域来确定变量名表达的信息量
*   package 只由小写字母组成，不可与标准库同名
*   重点考虑上下文信息，设计简洁清晰的名称

#### 控制流程

*   保持正常代码路径为最小缩进，优先处理错误情况来减少嵌套
*   故障问题大多出现在复杂的条件语句和循环语句中

#### 错误和异常处理

**error**

*   简单错误：仅出现一次且不被其他地方捕获的错误用`errors.New`表示
*   `fmt.Errorf`生成error错误链，方便定位error
*   判定一个错误是否为特定错误：`errors.Is`
*   在错误链上获取特定种类的错误：`errors.As`

**panic**

*   当程序启动阶段发生不可逆转的错误时，可以在init或main函数中使用panic
*   若问题可以被屏蔽或解决，使用error代替panic

**recover**

*   嵌套无法生效
*   生效范围在当前goroutine的被defer的函数中生效

### 性能优化建议

#### Benchmark

`go test -bench. -benchmem`

![uTools_1674051877213](bytedance-camp-note-3/uTools_1674051877213.png)

#### Slice

*   预分配空间效率更高
*   切片操作并不复制切片指向的元素
*   创建一个新的切片会复用原来切片的底层数组
*   使用从copy代替re-slice，防止大切片被小切片引用而不能释放
*   map与slice类似

#### 字符串

*   拼接：`strings.Builder&lt;bytes.Buffer&lt; +`
*   使用+拼接，每次都会重新分配内存，其余两种不会

#### 空结构体

*   空结构体`struct{}`实例不占据任何内存空间
*   空结构体本身具备很强的语义，不需要任何值，仅作为占位符
*   Set可以由map代替，只使用map的键，不用值

#### atomic/mutex

*   硬件实现，效率比锁高。锁的实现属于系统调用
*   sync.Mutex，应该用来保护一段逻辑，不仅仅保护一个变量

## 二、性能优化实战

### 性能分析工具pprof

#### 简介

![uTools_1674054566888](bytedance-camp-note-3/uTools_1674054566888.png)

#### pprof-排查实战

*   搭建pprof实践项目，来自[GitHub-go-pprof-practice](https://github.com/wolfogre/go-pprof-practice)
    
*   项目提前埋入一些炸弹代码，产生可观测的性能问题
    
*   CPU、堆内存、协程、锁、阻塞均有涉及
    

### 性能调优案例

从业务服务优化、基础库优化、Go语言优化三个方面展开

## 三、课后个人总结：

高质量编码需要大量的积累，我平常写代码尽量符合高质量编程原则，养成好的编码习惯，此外性能分析工具pprof需要在实战一步步掌握排查方法，仅仅靠一个案例是不够的。我会反复夯实基础课程，为后面的进阶课程做好准备。</content:encoded></item><item><title>高性能 Go 语言发行版优化的内存管理优化 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-4/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-4/</guid><description>高性能Go语言发行版的内存管理优化，深入理解自动垃圾回收与内存分配策略</description><pubDate>Thu, 19 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 一、自动内存管理：

### 概念

自动内存管理（垃圾回收Garbage Collector）：由程序语言的运行时系统管理动态内存

避免手动，专注于业务逻辑

保证内存使用的**正确性**和**安全性**

#### 三个任务

*   为新对象分配空间
*   找到存活对象
*   回收死亡对象的内存空间

#### 相关概念

*   Mutator: 业务线程，分配新对象，修改对象指向关系
*   Collector: GC线程，找到存活对象，回收死亡对象的内存空间
*   Serial GC: 只有一个collector
*   Parallel GC: 支持多个collectors同时回收的GC算法
*   Concurrent GC: mutator(s)和collector(s)可以同时执行
*   Collectors 必须感知对象指向关系的改变
*   Tracing garbage collection: 追踪垃圾回收
*   Reference counting: 引用计数
*   评价指标：安全、吞吐率、暂停时间、内存开销

### 追踪垃圾回收

#### 回收条件

指针指向关系不可达的对象

#### 回收过程

标记根对象-标记找到可达对象-清理所有不可达对象

根据对象的生命周期，使用不同的标记和清理策略

### 分代GC(Generational GC)

对年轻和老年的对象，制定不同的GC策略，降低整体内存管理的开销

不同年龄的对象处于heap的不同区域

#### 年轻代

由于存活对象很少，可以采用copying collection

#### 老年代

对象趋向于一直活着，反复复制开销较大，可以采用mark-sweep-collection

![uTools_1674141757938](bytedance-camp-note-4/uTools_1674141757938.png)

### 引用计数

对象存活条件：当且仅当引用数大于0

#### 优点

内存管理操作被平摊到程序执行过程中

不需要了解runtime的实现细节

#### 缺点

开销大，通过原子操作保证原子性和可见性

无法回收环形数据结构

回收内存时依然可能引发暂停

## 二、Go 内存管理及优化

### Go 内存分配

**缓存**

TCMalloc:thread caching

*   每个p包含一个mcache用于快速分配，用于为绑定于p上的g分配对象
*   mcache管理一组mspan
*   当mcache中的mspan分配完毕，向mcentral申请带有未分配块的mspan
*   当mspan中没有分配的对象，mspan会被缓存在mcentral中，而不是立刻释放并归还给OS

![uTools_1674141757938](bytedance-camp-note-4/uTools_1674141757938-169070666843037.png)

### Go 内存管理优化

对象分配是非常高频的操作：每秒分配GB级别的内存

**优化方案**

Balanced GC

Goroutine allocation buffer(GAB)对于Go内存管理来说是一个大对象

将多个小对象的分配合并成一次大对象的分配

实现了copying GC

## 三、编译器和静态分析

### 编译器的结构

![uTools_1674142824186](bytedance-camp-note-4/uTools_1674142824186.png)

### 静态分析

不执行程序的代码，推导程序的行为，分析程序的性质

控制流：程序执行的流程

数据流：数据在控制流上的传递

### 过程内和过程间分析

过程内分析(Intra-procedural analysis) 仅在函数内部进行分析 过程间分析(Inter-procedural analysis)

考虑函数调用时参数传递和返回值的数据流和控制流

*   需要通过数据流分析得知1的具体类型，才能知道1.foo()调用的 是哪个foo()
*   根据1的具体类型，产生了新的控制流，1.foo(),分析继续
*   过程间分析需要同时分析控制流和数据流一一联合求解，比较复杂

![uTools_1674143142814](bytedance-camp-note-4/uTools_1674143142814.png)

## 四、Go 编译器优化

#### 函数内联

消除函数调用开销

将过程间分析转化为过程内分析，帮助其他优化，例如逃逸分析

micro-benchmark验证可得性能提升明显

#### Beast Mode

调整函数内联的策略，使更多的函数被内联

#### 逃逸分析

分析代码中指针的动态作用域：指针在何处可以被访问

## 五、课后个人总结：

本次课程内容非常的偏向语言底层，需要一定编译原理的基础，有利于更好地理解。如今编译器优化还有很大的发展空间，课后有空多多阅读相关知识点</content:encoded></item><item><title>Go 框架三件套详解(Web/RPC/ORM) ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-5/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-5/</guid><description>Go语言Web/RPC/ORM框架三件套详解：GORM、Kitex与Hertz核心用法</description><pubDate>Tue, 31 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 一、Gorm：

### Gorm的约定

*   使用名为ID的字段作为主键
*   默认结构体的蛇形复数作为表名，字段名的蛇形作为列名
*   使用CreatedAt、UpdateAt字段作为创建、更新时间

### Gorm支持的数据库

*   MySQL、SQLServer、PostgreSQL、SQLite，通过驱动连接
    
    ```go
    import (
    &quot;gorm.io/driver/mysql&quot;
    &quot;gorm.io/gorm&quot;
    )
    
    db, err := gorm.Open(
        mysql.Open(&quot;root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&amp;parseTime=True&amp;loc=Local&quot;),
        &amp;gorm.Config{})
    ```
    

### 创建数据

*   使用default标签为字段定义默认值
*   使用clause.OnConflict处理数据冲突

```go
p:=&amp;Product{Code:&quot;D42&quot;,ID:1}
//不处理冲突
db.Create(p)
//处理冲突
db.clauses(clause.OnConflict{DoNothing:true}).Create(&amp;p)
```

### 查询数据

*   First查询单条数据失败返回ErrRecordNotFound，查询多条数据，查询不到不会返回错误
*   使用结构体作为查询条件，如果字段值为0、&apos;&apos;、false或其他零值，不会用于构建查询条件。需要使用Map来构建

```go
// SELECT * FROM users ORDER BY id LIMIT 1;
db.First(&amp;user)
// SELECT * FROM users WHERE name IN (&apos;jinzhu&apos;,&apos;jinzhu2&apos;);
db.Where(&quot;name IN ?&quot;,[]string{&quot;jinzhu&quot;,&quot;jinzhu2&quot;}).Find(&amp;users)
// SELECT * FROM users WHERE name LIKE &apos;%jin%&apos;;
db.Where(&quot;name LIKE ?&quot;,&quot;%jin%&quot;).Find(&amp;users)

// SELECT * FROM users WHERE name=&apos;jinzhu&apos;;
db.Where(&amp;User{name:&quot;jinzhu&quot;,Age:0}).Find(&amp;users)
// SELECT * FROM users WHERE name=&apos;jinzhu&apos;AND age=0;
db.Where(map[string]interface{}{&quot;name&quot;:&quot;jinzhu&quot;,&quot;Age&quot;:0}).Find(&amp;users)
```

### 更新数据

*   使用结构体作为更新零值同样需要使用Map来构建
*   SQL表达式更新
*   调用model！！不是db！！

```go
// UPDATE &quot;products&quot; SET &quot;price&quot;=price*2+100,&quot;updated_at&quot;=&apos;2013-11-17-21:34:10&apos; WHERE &apos;id&apos;=111;
db.model(&amp;User{ID:111}).Update(&quot;age&quot;,gorm.Expr(&quot;age*?+?&quot;,2,100))
```

### 删除数据

*   通过gorm.DeletedAt用于实现软删除，记录不会从数据库中真正删除，但会将DeletedAt置为当前时间，并且不能再通过正常的查询方法找到该记录
*   使用Unscoped可以查询软删除的数据

### 事务

*   通过Tansaction方法用于自动提交事务，避免用户漏写Commit、Rollback

```go
if err=db.Transaction(fun(tx *gorm.DB){
    if err=tx.create(&amp;User{Name:&quot;name&quot;}).Error;err!=nil{
        return err
    }
});err !=nil{
    return
}
```

### Hook

*   Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。
*   如果您已经为模型定义了指定的方法，它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误，GORM 将停止后续的操作并回滚事务。
*   钩子方法的函数签名应该是 `func(*gorm.DB) error`

```go
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

  if !u.IsValid() {
    err = errors.New(&quot;can&apos;t save invalid data&quot;)
  }
  return
}

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
  if u.ID == 1 {
    tx.Model(u).Update(&quot;role&quot;, &quot;admin&quot;)
  }
  return
}
```

### 性能提高

*   对于写操作（创建、更新、删除），为了确保数据的完整性，GORM 会将它们封装在事务内运行，但这会降低性能
*   执行任何 SQL 时都创建并缓存预编译语句，可以提高后续的调用速度

```go
db, err := gorm.Open(sqlite.Open(&quot;gorm.db&quot;), &amp;gorm.Config{
  SkipDefaultTransaction: true,
  PrepareStmt: true
})
```

## 二、Kitex

### 关于 Kitex

Kitex 是一个 RPC 框架，既然是 RPC，底层就需要两大功能：

1.  Serialization 序列化
2.  Transport 传输

Kitex 框架及命令行工具，默认支持 `thrift` 和 `proto3` 两种 IDL，对应的 Kitex 支持 `thrift` 和 `protobuf` 两种序列化协议。 传输上 Kitex 使用扩展的 `thrift` 作为底层的传输协议（注：thrift 既是 IDL 格式，同时也是序列化协议和传输协议）。IDL 全称是 Interface Definition Language，接口定义语言。

### 运行环境

VMware Centos7

具体安装和网络配置参照文章，可以实现手机连接同一wifi测试虚拟机内抖音项目demo

[Centos7.7安装及配置教程 - 掘金 (juejin.cn)](https://juejin.cn/post/6844904101583519752)

期间踩了无数坑，还碰巧给虚拟机配和手机相同ip，电脑测试正常，手机网络异常，大家千万要避坑！！

### thrift版本的问题

由于thrift最新版本与kitex不兼容，所以需要输入以下命令切换版本

```bash
go mod edit -replace github.com/apache/thrift=github.com/apache/thrift@v0.13.0
```

### DEMO运行

具体步骤参照[快速开始 CloudWeGo](https://www.cloudwego.io/zh/docs/kitex/getting-started/#基础教程)

## 三、Hertz

### 基本使用

```go
package main

import (
    &quot;context&quot;

    &quot;github.com/cloudwego/hertz/pkg/app&quot;
    &quot;github.com/cloudwego/hertz/pkg/app/server&quot;
    &quot;github.com/cloudwego/hertz/pkg/common/utils&quot;
    &quot;github.com/cloudwego/hertz/pkg/protocol/consts&quot;
)

func main() {
    h := server.Default()

    h.GET(&quot;/ping&quot;, func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{&quot;message&quot;: &quot;pong&quot;})
    })

    h.Spin()
}
```

### 路由

*   Hertz提供了GET、POST、PUT、DELETE、ANY等方法用于注册路由。
*   Hertz提供了路由组(Group的能力，用于支持路由分组的功能
*   Hrtz提供了参数路由和通配路由，路由的优先级为：静态路由&gt;命名路由&gt;通配路由

### 参数绑定

Hertz提供了Bind、Validate、BindAndValidate函数用于进行参数绑定和校验

### 中间件

中间件会按定义的先后顺序依次执行，如果想快速终止中间件调用，可以使用以下方法，注意**当前中间件仍将执行**。

*   `Abort()`：终止后续调用
*   `AbortWithMsg(msg string, statusCode int)`：终止后续调用，并设置 response中body，和状态码
*   `AbortWithStatus(code int)`：终止后续调用，并设置状态码

```go
// 方式一
func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle
    // ...
    c.Next(ctx)
  }
}

// 方式二
func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    c.Next(ctx) // call the next middleware(handler)
    // post-handle
    // ...
  }
}
```

### 代码生成工具

Hertz提供了代码生成工具Hz,通过定义IDL文件即可生成对应的基础服务代码

## 四、笔记项目实战

阅读笔记项目的代码并可以在本地环境运行起来

## 五、课后个人总结：

课后要进一步熟悉Gorm/Kitex/Hertz的使用，尤其是关注官方文档

多多阅读笔记项目的代码，以便在后续大作业进行参考</content:encoded></item><item><title>规则引擎设计与实现 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-6/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-6/</guid><description>规则引擎的设计原理与实现，将业务决策从应用代码中抽离的架构模式</description><pubDate>Tue, 31 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 一、认识规则引擎

### 定义

嵌入在应用中的组件，将业务决策从应用中抽离出来，并使用预定义的语义编写决策。接受数据输入，解释业务规则，做出决策

### 组成

数据输入 -- 支持接受使用预定义的语义编写的规则作为策略集，接受业务的数据作为参数

规则理解 -- 按预先定义的词法正确理解业务规则的语义

规则执行 -- 类型检查

### 应用场景

风控对抗

活动运营

数据分析及数据清洗

## 二、编译原理

### 词法分析

把源代码字符串转化为词法单元Token

基于有限自动机，状态转换

### 语法分析

在词法分析的基础上，识别语法结构

基于抽象语法树（上下文无关语法）

![](bytedance-camp-note-6/uTools_1675265268705-300x161.png)

### 类型检查

综合检查

*   根据子表达式构造父表达式的类型

运行/编译时检查

*   编译时提前声明
*   执行时根据传入的参数的类型，在执行过程中检查

## 三、设计一个规则引擎

### 设计目标

设计一个规则引擎，支持特定的词法、运算符、数据类型和优先级。并且支持基于以上预定义语法的规则表达式的编译和执行。

*   词法分析
*   运算符
*   数据类型
*   优先级

### 词法分析

```
参数：由字母数字下划线组成eg:_ab2、user_name
布尔值：true、false
字符串：&quot;abcd&quot;、&apos;abcd&apos;、`abcd`
十进制int:1234
十进制float:123.5
一元运算符：+ -
二元运算符：+ - * / % &gt; &lt; &gt;= &lt;= == !=
逻辑操作符：&amp;&amp;  !
括号：()
```

### 语法分析

*   优先级的表达
*   语法树结构
*   递归下降

### 语法树执行与类型检查

*   语法树执行 -- 后续遍历
*   类型检查 -- 在节点左右子节点执行完成后分别校验类型是否符合预设

## 四、实现规则引擎

项目地址：[简单的规则引擎](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fqimengxingyuan%2Fyoung_engine)

运行：

```bash
docker-compose up
go run ./main.go
```

默认端口是 `:8888` 测试一下

```go
func Compiler(exp string) (*executor.Node, error) {
    // 扫描语句
    tokenScanner := compiler.NewScanner(exp)
    // 词法分析
    tokens, err := tokenScanner.Lexer()
    if err != nil {
        return nil, err
    }
    // 语法分析
    parser := compiler.NewParser(tokens)
    err = parser.ParseSyntax()
    if err != nil {
        return nil, err
    }
    // 优先级判断
    astBuilder := compiler.NewBuilder(parser)
    ast, err := astBuilder.Build()
    if err != nil {
        return nil, err
    }

    return ast, nil
}
```

## 五、总结

本次课程内容非常的偏向语言底层，需要一定编译原理的基础，有利于更好地理解。课后有空多多阅读代码，实现一个web版规则引擎</content:encoded></item><item><title>架构初探-谁动了我的蛋糕 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-7/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-7/</guid><description>软件架构初探：从单机到分布式，理解架构演进的驱动力与设计取舍</description><pubDate>Thu, 02 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 一、什么是架构

### 定义

有关软件整体结构与组件的抽象化描述

### 架构演进

*   单机架构
    
    把所有功能实现在一个进程里，部署在一台机器上
    
    优点：简单，只有简单
    
*   单体架构，垂直切分
    
    分布式部署，按应用垂直切分单体
    
    优点：水平扩容，运维不需要停服
    
    ![uTools_1675351723438](bytedance-camp-note-7/uTools_1675351723438.png)
    
*   **SOA(Service-Oriented Architecture)，微服务**
    
    把功能抽象为不同服务，并且每个服务之间有通讯标准但是soa和微服务还是有不同的，在于通讯方式，数据库设计和服务规模
    
    优点：1.不同模块的RD可以专心于自己的业务逻辑了，开发迭代效率得到显著提高
    
    2.各个服务独立运维，变更操作的影响面可控，应用整体的稳定性得到了提高
    
    ![uTools_1675351791907](bytedance-camp-note-7/uTools_1675351791907.png)
    

## 二、企业级后端架构剖析

### 云计算

**IaaS：Infrastructure as a Service（基础设施即服务）**

*   买房子vs房屋租赁平台

从上面的架构图可以看出，IaaS处于最底层，服务商提供底层/物理层基础设施资源（服务器，数据中心，环境控制，电源，服务器机房），客户自己部署和执行操作系统或应用程序等各种软件。

**PaaS：Platform as a Service（平台即服务）**

*   清包vs全包

PaaS处于中间层，服务商提供基础设施底层服务，提供操作系统（Windows，Linux）、数据库服务器、Web服务器、域控制器和其他中间件，以及服务模型中的备份服务等中件层服务。例如IIS，.NET，Apache，MySQL …，客户自己控制上层的应用程序部署与应用托管的环境。

**SaaS：Software as a Service（软件即服务）**

*   从零培训vs雇佣培训过的师傅

SaaS处于最上层，服务商提供基于软件的解决方案，满足客户最终需求；如OA、CRM、MIS、ERP、HRM、CM、Office 365、iCloud、G Suite等应用，客户不需考虑任何形式的专业技术知识，获得完整的软件包，使他们的日常工作和生活变得更轻松。

**FaaS：Function as a service（函数即服务）**

*   纯手工制作vs蛋糕机批量生产

服务商提供一个平台，允许客户开发、运行和管理应用程序功能，而无需构建和维护通常与开发和启动应用程序相关的基础架构的复杂性。 按照此模型构建应用程序是实现“无服务器”体系结构的一种方式，通常在构建微服务应用程序时使用。

![uTools_1675352194860](bytedance-camp-note-7/uTools_1675352194860.png)

### 云原生

云原生是一种**构建和运行应用程序的方法**

云原生主要涉及四个方面：**DevOps，弹性资源，微服务架构，服务网格**

**![uTools_1675352144403](bytedance-camp-note-7/uTools_1675352144403.png)**

## 三、企业级后端架构的挑战

### 离在线资源并池

**在线业务的特点**

*   IO密集型为主
*   潮汐性、实时性

**离线业务的特点**

*   计算密集型占多数
*   非实时性

### 自动扩缩容

利用在线业务潮汐性自动扩缩容来降低业务成本

### 微服务亲和性部署

*   将满足亲合性条件的容器调度到一台宿主机
*   微服务中间件与服务网格通过共享内存通信
*   服务网格控制面实施灵活、动态的流量调度

### 流量治理

基于微服务中间件&amp;服务网格的流量治理

提高容错性、容灾

### CPU水位负载均衡

**Iaas**

提供资源探针

**服务网格**

动态负载均衡

## 四、总结

没有最好的架构，只有最合适的架构。

学习架构设计的过程以及其中的问题，可以很好帮助我们理解项目的构建的目的、过程。让我的思维更加全面，目光更加长远。</content:encoded></item><item><title>分布式理论-现代架构基石 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-8/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-8/</guid><description>分布式系统理论基础：CAP定理、一致性协议与共识算法等现代架构基石</description><pubDate>Thu, 02 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 一、分布式概述

### 什么是分布式

分布式系统是计算机程序的集合，这些程序利用跨多个独立计算节点的计算资源来实现共同的目标。可以分为分布式计算、分布式存储、分布式数据库等。

优势：

*   去中心化
*   低成本
*   弹性
*   资源共享
*   可靠性高

挑战：

*   普遍的节点故障
*   不可靠的网络
*   异构的机器与硬件环境
*   安全

## 二、系统模型

### 故障模型：

*   Byzantine failure:节点可以任意篡改发送给其他节点的数据
    
*   Authentication detectable byzantine failure
    
    (ADB):Byzantine failure的特例;节点可以篡改数据，但不能伪造其他节点的数据
    
*   Performance failure:节点未在特定时间段内收到数据，即时间太早或太晚
    
*   Omission failure:节点收到数据的时间无限晚，即收不到数据
    
*   Crash failure:在omission failure的基础上，增加了节点停止响应的假设，也即持续性地omission failure
    
*   Fail-stop failure:在Crash failure的基础上增加了错误可检测的假设
    

![uTools_1675429622380](bytedance-camp-note-8/uTools_1675429622380.png)

### 拜占庭将军问题

由于当时拜占庭罗马帝国国土辽阔，为了达到防御目的，每个军队都分隔很远，将军与将军之间只能靠信差传消息。在战争的时候，拜占庭军队内所有将军和副官必须达成一致的共识，决定是否有赢的机会才去攻打敌人的阵营。但是，在军队内有可能存有叛徒和敌军的间谍，左右将军们的决定又扰乱整体军队的秩序。在进行共识时，结果并不代表大多数人的意见。这时候，在已知有成员谋反的情况下，其余忠诚的将军在不受叛徒的影响下如何达成一致的协议。

结论是，拜占庭将军问题是被证实无解的电脑通信问题，两支军队理论上永远无法达成共识。

TCP三次握手是在两个方向确认包的序列号，增加了超时重试，是两将军问题的一个工程解。

**思考**

1.TCP握手为什么不是两次？ 根本原因: 无法确认客户端的接收能力。

分析如下:

如果是两次，你现在发了 SYN 报文想握手，但是这个包滞留在了当前的网络中迟迟没有到达，TCP 以为这是丢了包，于是重传，两次握手建立好了连接。

看似没有问题，但是连接关闭后，如果这个滞留在网路中的包到达了服务端呢？这时候由于是两次握手，服务端只要接收到然后发送相应的数据包，就默认建立连接，但是现在客户端已经断开了，这就带来了连接资源的浪费。

2.TCP握手为什么不是四次？ 三次握手的目的是确认双方发送和接收的能力，那四次握手可以嘛？当然可以，100 次都可以。但为了解决问题，三次就足够了，再多用处就不大了。

3.TCP挥手过程中,如果FIN报文丢失,发生什么?

当客户端（主动关闭方）调用 close 函数后，就会向服务端发送 FIN 报文，试图与服务端断开连接，此时客户端的连接进入到 FIN\_WAIT\_1 状态。

正常情况下，如果能及时收到服务端（被动关闭方）的 ACK，则会很快变为 FIN\_WAIT2状态。

如果第一次挥手丢失了，那么客户端迟迟收不到被动方的 ACK 的话，也就会触发超时重传机制，重传 FIN 报文，重发次数由 tcp\_orphan\_retries 参数控制。

当客户端重传 FIN 报文的次数超过 tcp\_orphan\_retries 后，就不再发送 FIN 报文，则会在等待一段时间（时间为上一次超时时间的 2 倍），如果还是没能收到第二次挥手，那么直接进入到 close 状态。

### 共识和一致性

*   最终一致性：客户端A写完成后，其他客户端读到一致的数据
*   线性一致性：客户端A读到新值后及时同步给其他客户端，需要节点间协商，增加了延迟，系统可用性会受损

## 三、理论基础

### CAP理论

CAP理论往往应用于数据库领域，同样可以适用于分布式存储方向。

C：一致性,指数据在多个副本之间能够保持一致的特性（严格的一致性)。

A：可用性，指系统提供的服务必须一直处于可用的状态，每次请求都能获取到非错的响应———但是不保证获取的数据为最新数据。

P：分区容错性，分布式系统在遇到任何网络分区故障的时候，仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。

二、BASE理论 BASE理论是基于CAP理论逐步演化而来的，是CP（强一致性）和AP（强可用性）权衡的结果。 BASE理论的核心思想是：即使无法做到强一致性，但每个应用都可以根据自身业务特点采用适当的方式来使系统达到最终一致性。Basically Available（基本可用） 响应时间上的损失：正常情况下，处理用户请求需要0.5s返回结果，但是由于系统出现故障，处理用户请求的时间变成3s。 系统功能上的损失：正常情况下，用户可以使用系统的全部功能，但是由于系统访问量突然剧增，系统的非核心功能无法使用。 Soft state（软状态） 数据同步允许一定的延迟。 Eventually consistent（最终一致性） 系统中所有的数据副本，在经过一段时间的同步后，最终能够达到一个一致的状态，不要求实时。

## 四、分布式事务

二阶段提交（Two-phase Commit):为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种演算法。

三个假设:

1.  引入协调者(Coordinator)和参与者（Participants)，互相进行网络通信
2.  所有节点都采用预写式日志，且日志被写入后即被保持在可靠的存储设备上
3.  所有节点不会永久性损坏，即使损坏后仍然可以恢复

## 五、共识协议

Quorum NWR协议 ：是一种简化版的一致性模型

RAFT协议：保证了出现部分节点故障，也不会影响各节点，一定意义使用Quorum协议

**强调一致性！！**

## 六、课后人个总结

通过今天的学习我了解到了分布式理论，其中很多是学习数据库和计网时的知识点，分布式遇到的最大问题就是数据一致性的问题，每个理论、模型甚至协议，均致力于实现数据的一致性。</content:encoded></item><item><title>微服务架构原理与治理实践 ｜ 字节跳动青训营笔记</title><link>https://kaitolab.net/blog/bytedance-camp-note-9/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-9/</guid><description>微服务架构原理与服务治理实践，从单体拆分到服务注册发现的完整路径</description><pubDate>Fri, 03 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 一、微服务架构介绍

### 系统架构演变历史

**单体架构**

优势：性能最高、冗余小

劣势：debug困难、模块相互影响、开发流程

![uTools_1675509142953](bytedance-camp-note-9/uTools_1675509142953.png)

**垂直应用架构**

优势：业务独立开发维护

劣势：不同业务单体且存在冗余

![垂直应用架构](bytedance-camp-note-9/垂直应用架构.png)

**分布式架构**

优势：业务无关的独立服务

劣势：服务模块bug可导致全站瘫痪、调用关系复杂、不同服务冗余

![分布式架构](bytedance-camp-note-9/分布式架构.png)

**SOA架构（Service Oriented Architecture）**

优势：服务注册

劣势：中心化、从上至下设计、重构困难

![SOA架构](bytedance-camp-note-9/SOA架构.png)

**微服务架构**

优势：开发效率、业务独立设计、自下而上、故障隔离

劣势：治理运维难度、观测挑战、安全性、分布式系统

![微服务架构](bytedance-camp-note-9/微服务架构.png)

### 微服务架构核心要素

*   服务治理：服务注册、服务发现、负载均衡、扩缩容、流量治理、稳定性治理
*   可观测性：日志采集、日志分析、监控打点、监控大盘、异常报警、链路追踪
*   安全：身份验证、认证授权、访问令牌、审计、传输加密、黑产攻击

## 二、微服务架构原理及特征

### 服务注册及发现

**hardcode**

hardcode的方式下指定下游实例地址的问题

服务有多个实例，没法hardcode，不同服务实例ip port本身是动态变化的，无法实现

**DNS**

问题：

*   本地DNS存在缓存、导致延时
*   负载均衡问题
*   不支持服务实例的探活检查
*   域名无法配置端口

解决思路：

新增一个统一的服务注册中心，用于存储服务名到服务实例的映射

![服务注册及发现](bytedance-camp-note-9/服务注册及发现.png)

### 流量特征

*   统一网管入口
*   内网通信多数采用RPC
*   网状调用链路

## 三、核心服务治理功能

### 服务发布

即指让一个服务升级运行新的代码的过程

**服务发布的难点**

服务不可用、服务抖动、服务回滚

灰度发布（金丝雀发布）

![uTools_1675522295198](bytedance-camp-note-9/uTools_1675522295198.png)

### 流量治理

在微服务架构下，我们可以基于地区、集群、实例、请求等维度，对端到端流量的路由路径进行精确控制

### 负载均衡

负责分配请求在每个下游实例上的分布

常见LB策略

*   Round Robin
*   Random
*   Ring Hash
*   Least Request

## 四、字节跳动服务治理实践

## 五、课后人个总结

通过今天的学习我了解到了分布式理论，其中很多是学习数据库和计网时的知识点，分布式遇到的最大问题就是数据一致性的问题，每个理论、模型甚至协议，均致力于实现数据的一致性。</content:encoded></item><item><title>青训营 X 码上掘金 攒青豆 Golang</title><link>https://kaitolab.net/blog/bytedance-camp-note-beans/</link><guid isPermaLink="true">https://kaitolab.net/blog/bytedance-camp-note-beans/</guid><description>青训营×码上掘金「攒青豆」算法题——Golang实现接雨水变体问题</description><pubDate>Mon, 06 Feb 2023 16:00:00 GMT</pubDate><content:encoded># 青训营 X 码上掘金 攒青豆 Golang

**当青训营遇上码上掘金**

## 题目描述

现有 n 个宽度为 1 的柱子，给出 n 个非负整数依次表示柱子的高度，排列后如下图所示，此时均匀从上空向下撒青豆，计算按此排列的柱子能接住多少青豆。（不考虑边角堆积）

![攒青豆.png](bytedance-camp-note-beans/攒青豆.webp)

```css
以下为上图例子的解析：

输入：height = [5,0,2,1,4,0,1,0,3]  
输出：17  
解析：上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度，在这种情况下，可以接 17 个单位的青豆。
```

## 解题思路

本人刷题用c++较多，由于本次青训营学习的语言为Golang，故采用Go来解题。

题目给出n个柱子及对应的高度，由于柱子高低不同，导致我们难以判断两个柱子之间会积攒多少青豆。但是我们如果将柱子简化到高度只有1，那么可以容易得到积攒的青豆数。

我们对上图的例子进行分析。得到第一层height1 = \[1,0,1,1,1,0,1,0,1\] ，容易得知第一层青豆数为3

![uTools_1675753557445.png](bytedance-camp-note-beans/uTools_1675753557445-300x31.png)

之后剩余部分为 height2 = \[4,0,1,0,3,0,0,0,2\]

![uTools_1675753841021](bytedance-camp-note-beans/uTools_1675753841021.png)

如此一来，我们得到了一个新的height2，我们可以通过递归求解，也可以自底向上一层一层求解。我采用了第二种。

层数

青豆数

1

3

2

5

3

6

4

3

5

0

合计

17

## 代码

[为了青豆 - 码上掘金 (juejin.cn)](https://code.juejin.cn/pen/7189615352114839612)

```go
package main

import &quot;fmt&quot;

func main() {
  // example :
  // n:=9
  // height := [...]int{5,0,2,1,4,0,1,0,3}
  var n int
  fmt.Scanf(&quot;%d&quot;, &amp;n)
  height:=make([]int,1000)
  for i:= 0; i &lt; n; i++ {
    fmt.Scanf(&quot;%d&quot;, &amp;height[i])
  }
  sum:=0
  flag:=1
  for flag==1{
    flag=0
    count:=0
    for i := 0; i &lt; n; i++ {
      if height[i]&gt;0{
        if flag==0{
          flag=1
          height[i]--
        }else{
          height[i]--
          sum+=count
          count=0
        }
      }else if height[i]&lt;=0 &amp;&amp; flag==1{
        count++
      }
    }
  }
  fmt.Printf(&quot;青豆数:%d&quot;,sum)
}
```

## 编码注意事项

*   每一层的豆子需要由两边的柱子挡住，如果柱子数为0或1，青豆数为0，我采用flag来进行标记，flag初始为0，找到第一个柱子时flag=1，并开始计数。
*   外层循环停止条件，当遍历完数组后，flag仍为0，则表明最高层的青豆数已经计算完毕，退出循环。
*   我是从横向的角度分析青豆数，也可以从纵向的角度分析，每个柱子上方的青豆数与左侧最高柱子高度和右侧最高柱子高度中的最小值有关。</content:encoded></item><item><title>CSP 201612-1中间数 C++ 100</title><link>https://kaitolab.net/blog/csp-201612-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201612-1/</guid><description>【问题描述】在一个整数序列a1, a2, …, an中，如果存在某个数，大于它的整数数量等于小于它的整数数量，则称其为中间数。在一个序列中，可能存在多个下标不相同的中间数，这些中间数的值是相同的。 　　给定一个整数序列，请找出这个整数...</description><pubDate>Thu, 29 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【问题描述】在一个整数序列a1, a2, …, an中，如果存在某个数，大于它的整数数量等于小于它的整数数量，则称其为中间数。在一个序列中，可能存在多个下标不相同的中间数，这些中间数的值是相同的。 　　给定一个整数序列，请找出这个整数序列的中间数的值。

【输入格式】输入的第一行包含了一个整数n，表示整数序列中数的个数。 　　第二行包含n个正整数，依次表示a1, a2, …, an。 

【输出格式】 如果约定序列的中间数存在，则输出中间数的值，否则输出-1表示不存在中间数。

【样例输入】

```
6
2 6 5 6 3 5
```

【样例输出】

```
5
```

【样例说明】 　　比5小的数有2个，比5大的数也有2个。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;
int main()
{
    int n;
    int a[1005];
    int mid;
    int i,j;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;a[i];
    }
    sort(a,a+n);
    mid=n/2;
    for(i=mid-1;a[i]==a[mid]&amp;&amp;i&gt;=0;i--);
    for(j=mid+1;a[j]==a[mid]&amp;&amp;j&lt;n;j++);
    if(i+1==n-j)
    {
        cout&lt;&lt;a[mid]&lt;&lt;endl;
    }
    else
    {
        cout&lt;&lt;-1&lt;&lt;endl;
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 201803-1跳一跳 C++ 100</title><link>https://kaitolab.net/blog/csp-201803-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201803-1/</guid><description>【问题描述】近来，跳一跳这款小游戏风靡全国，受到不少玩家的喜爱。简化后的跳一跳规则如下：玩家每次从当前方块跳到下一个方块，如果没有跳到下一个方块上则游戏结束。如果跳到了方块上，但没有跳到方块的中心则获得1分；跳到方块中心时，若上一次的...</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【问题描述】近来，跳一跳这款小游戏风靡全国，受到不少玩家的喜爱。简化后的跳一跳规则如下：玩家每次从当前方块跳到下一个方块，如果没有跳到下一个方块上则游戏结束。如果跳到了方块上，但没有跳到方块的中心则获得1分；跳到方块中心时，若上一次的得分为1分或这是本局游戏的第一次跳跃则此次得分为2分，否则此次得分比上一次得分多两分（即连续跳到方块中心时，总得分将+2，+4，+6，+8...）。现在给出一个人跳一跳的全过程，请你求出他本局游戏的得分（按照题目描述的规则）。 

【输入格式】输入包含多个数字，用空格分隔，每个数字都是1，2，0之一，1表示此次跳跃跳到了方块上但是没有跳到中心，2表示此次跳跃跳到了方块上并且跳到了方块中心，0表示此次跳跃没有跳到方块上（此时游戏结束）。 

【输出格式】输出一个整数，为本局游戏的得分（在本题的规则下）。 

【样例输入】

```
1 1 2 2 2 1 1 2 2 0
```

【样例输出】

```
22
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

int main()
{
    int score=0;
    int total=0;
    int x;
    while(cin&gt;&gt;x)
    {
        if(x==0)
            break;
        else if(x==1)
        {
            score=1;
        }
        else
        {
            if(total==0||score==1)
            {
                score=2;
            }
            else
            {
                score+=2;
            }
        }
        total+=score;
    }
    cout&lt;&lt;total&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 201803-2 碰撞的小球 C++ 100</title><link>https://kaitolab.net/blog/csp-201803-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201803-2/</guid><description>【问题描述】数轴上有一条长度为L（L为偶数)的线段，左端点在原点，右端点在坐标L处。有n个不计体积的小球在线段上，开始时所有的小球都处在偶数坐标上，速度方向向右，速度大小为1单位长度每秒。当小球到达线段的端点（左端点或右端点）的时候，...</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【问题描述】数轴上有一条长度为L（L为偶数)的线段，左端点在原点，右端点在坐标L处。有n个不计体积的小球在线段上，开始时所有的小球都处在偶数坐标上，速度方向向右，速度大小为1单位长度每秒。当小球到达线段的端点（左端点或右端点）的时候，会立即向相反的方向移动，速度大小仍然为原来大小。当两个小球撞到一起的时候，两个小球会分别向与自己原来移动的方向相反的方向，以原来的速度大小继续移动。现在，告诉你线段的长度L，小球数量n，以及n个小球的初始位置，请你计算t秒之后，各个小球的位置。 
提示：因为所有小球的初始位置都为偶数，而且线段的长度为偶数，可以证明，不会有三个小球同时相撞，小球到达线段端点以及小球之间的碰撞时刻均为整数。同时也可以证明两个小球发生碰撞的位置一定是整数（但不一定是偶数）。 

【输入格式】输入的第一行包含三个整数n, L, t，用空格分隔，分别表示小球的个数、线段长度和你需要计算t秒之后小球的位置。第二行包含n个整数a1, a2, …, an，用空格分隔，表示初始时刻n个小球的位置。 

【输出格式】输出一行包含n个整数，用空格分隔，第i个整数代表初始时刻位于ai的小球，在t秒之后的位置。 

【样例输入】

```
3 10 5
4 6 8
```

【样例输出】

```
7 9 9
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

struct ball
{
    int id;
    int x;
    int dir=1; //left -1; right 1
}b[100];

bool cmp(ball A, ball B)
{
    return A.x&lt;B.x;
}

bool cmp2(ball A, ball B)
{
    return A.id&lt;B.id;
}

int main()
{
    int n,L,t;
    int foreball;
    cin&gt;&gt;n&gt;&gt;L&gt;&gt;t;
    for(int i=0;i&lt;n;i++)
    {
        b[i].id=i;
        cin&gt;&gt;b[i].x;
        if(b[i].x==L)
            b[i].dir=-1;
    }

    sort(b,b+n,cmp);
    while(t--)
    {
        foreball=-1;
        for(int i=0;i&lt;n;i++)
        {
            b[i].x+=b[i].dir;
            if(b[i].x==Lb[i].x==0)
                b[i].dir=-b[i].dir;
            else if(b[i].x==foreball)
            {
                b[i].dir=-b[i].dir;
                b[i-1].dir=-b[i-1].dir;
            }
            foreball=b[i].x;
        }
    }
    sort(b,b+n,cmp2);
    for(int i=0;i&lt;n;i++)
    {
        cout&lt;&lt;b[i].x&lt;&lt;&quot; &quot;;
    }

    return 0;
}
```</content:encoded></item><item><title>CSP 201812-1小明上学 C++ 100</title><link>https://kaitolab.net/blog/csp-201812-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201812-1/</guid><description>【题目背景】小明是汉东省政法大学附属中学的一名学生，他每天都要骑自行车往返于家和学校。为了能尽可能充足地睡眠，他希望能够预计自己上学所需要的时间。他上学需要经过数段道路，相邻两段道路之间设有至多一盏红绿灯。京州市的红绿灯是这样工作的：...</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】小明是汉东省政法大学附属中学的一名学生，他每天都要骑自行车往返于家和学校。为了能尽可能充足地睡眠，他希望能够预计自己上学所需要的时间。他上学需要经过数段道路，相邻两段道路之间设有至多一盏红绿灯。京州市的红绿灯是这样工作的：每盏红绿灯有红、黄、绿三盏灯和一个能够显示倒计时的显示牌。假设红绿灯被设定为红灯 r 秒，黄灯 y 秒，绿灯 g 秒，那么从 0 时刻起，\[0,r) 秒内亮红灯，车辆不许通过；\[r, r+g) 秒内亮绿灯，车辆允许通过；\[r+g, r+g+y) 秒内亮黄灯，车辆不许通过，然后依次循环。倒计时的显示牌上显示的数字 l（l &gt; 0）是指距离下一次信号灯变化的秒数。 

【问题描述】一次上学的路上，小明记录下了经过每段路的时间，和各个红绿灯在小明到达路口时的颜色和倒计时秒数。希望你帮忙计算此次小明上学所用的时间。 

【输入格式】输入的第一行包含空格分隔的三个正整数 r、y、g，表示红绿灯的设置。这三个数均不超过 106。输入的第二行包含一个正整数 n（n ≤ 100），表示小明总共经过的道路段数和看到的红绿灯数目。接下来的 n 行，每行包含空格分隔的两个整数 k、t。k=0 表示经过了一段道路，耗时 t 秒，此处 t 不超过 106；k=1、2、3 时，分别表示看到了一个红灯、黄灯、绿灯，且倒计时显示牌上显示的数字是 t，此处 t 分别不会超过 r、y、g。

【输出格式】输出一个数字，表示此次小明上学所用的时间。 

【样例输入】

```
30 3 30
8
0 10
1 5
0 11
2 2
0 6
0 3
3 10
0 3
```

【样例输出】

```
70
```

【样例说明】小明先经过第一段道路，用时 10 秒，然后等待 5 秒的红灯，再经过第二段道路，用时 11 秒，然后等待 2 秒的黄灯和 30 秒的红灯，再经过第三段、第四段道路，分别用时6、3秒，然后通过绿灯，再经过最后一段道路，用时 3 秒。共计 10 + 5 + 11 + 2 + 30 + 6 + 3 + 3=70 秒。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

int main()
{
    int r,y,g,n;
    int k,t,sum=0;
    cin&gt;&gt;r&gt;&gt;y&gt;&gt;g;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;k&gt;&gt;t;
        if(k==0)
            sum+=t;
        else if(k==1)
            sum+=t;
        else if(k==2)
            sum=sum+r+t;
    }
    cout&lt;&lt;sum;
    return 0;
}
```</content:encoded></item><item><title>CSP 201812-2小明放学 C++ 100</title><link>https://kaitolab.net/blog/csp-201812-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201812-2/</guid><description>【题目背景】汉东省政法大学附属中学所在的光明区最近实施了名为“智慧光明”的智慧城市项目。具体到交通领域，通过“智慧光明”终端，可以看到光明区所有红绿灯此时此刻的状态。小明的学校也安装了“智慧光明”终端，小明想利用这个终端给出的信息，估...</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】汉东省政法大学附属中学所在的光明区最近实施了名为“智慧光明”的智慧城市项目。具体到交通领域，通过“智慧光明”终端，可以看到光明区所有红绿灯此时此刻的状态。小明的学校也安装了“智慧光明”终端，小明想利用这个终端给出的信息，估算自己放学回到家的时间。 

【问题描述】一次放学的时候，小明已经规划好了自己回家的路线，并且能够预测经过各个路段的时间。同时，小明通过学校里安装的“智慧光明”终端，看到了出发时刻路上经过的所有红绿灯的指示状态。请帮忙计算小明此次回家所需要的时间。 

【输入格式】输入的第一行包含空格分隔的三个正整数 r、y、g，表示红绿灯的设置。这三个数均不超过 106。输入的第二行包含一个正整数 n，表示小明总共经过的道路段数和路过的红绿灯数目。接下来的 n 行，每行包含空格分隔的两个整数 k、t。k=0 表示经过了一段道路，将会耗时 t 秒，此处 t 不超过 106；k=1、2、3 时，分别表示出发时刻，此处的红绿灯状态是红灯、黄灯、绿灯，且倒计时显示牌上显示的数字是 t，此处 t 分别不会超过 r、y、g。 

【输出格式】输出一个数字，表示此次小明放学回家所用的时间。 

【样例输入】

```
30 3 30
8
0 10
1 5
0 11
2 2
0 6
0 3
3 10
0 3
```

【样例输出】

```
46
```

【样例说明】 　　小明先经过第一段路，用时 10 秒。第一盏红绿灯出发时是红灯，还剩 5 秒；小明到达路口时，这个红绿灯已经变为绿灯，不用等待直接通过。接下来经过第二段路，用时 11 秒。第二盏红绿灯出发时是黄灯，还剩两秒；小明到达路口时，这个红绿灯已经变为红灯，还剩 11 秒。接下来经过第三、第四段路，用时 9 秒。第三盏红绿灯出发时是绿灯，还剩 10 秒；小明到达路口时，这个红绿灯已经变为红灯，还剩两秒。接下来经过最后一段路，用时 3 秒。共计 10+11+11+9+2+3 = 46 秒。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;
using ll = long long;

int main()
{
    ll r,y,g,n;
    ll k,t,sum=0,nowtime=0;
    cin&gt;&gt;r&gt;&gt;y&gt;&gt;g;
    cin&gt;&gt;n;
    for(ll i=0;i&lt;n;i++)
    {
        cin&gt;&gt;k&gt;&gt;t;
        if(k==0)
            nowtime=-1;
        else if(k==1)
            nowtime=(sum+r-t)%(r+g+y);
        else if(k==2)
            nowtime=(sum+r+g+y-t)%(r+g+y);
        else
            nowtime=(sum+r+g-t)%(r+g+y);

        if(nowtime==-1)
            sum+=t;
        else if(nowtime&lt;r)
            sum+=r-nowtime;
        else if(nowtime&lt;r+g)
            ;
        else
            sum+=r+g+y-nowtime+r;
    }
    cout&lt;&lt;sum;
    return 0;
}
```</content:encoded></item><item><title>CSP 201903-1小中大 C++ 100</title><link>https://kaitolab.net/blog/csp-201903-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201903-1/</guid><description>【题目背景】 在数据分析中，最小值最大值以及中位数是常用的统计信息。</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 在数据分析中，最小值最大值以及中位数是常用的统计信息。 

【题目描述】 老师给了你个整数组成的测量数据，保证有序（可能为升序或降序），可能存在重复的数据。请统计出这组测量数据中的最大值、中位数以及最小值，并按照从大到小的顺序输出这三个数 

【输入格式】 从标准输入读入数据。第一行输入一个整数，在第二行中存在n个有序的整数，表示测量数据，可能为升序或降序排列，可能存在连续多个整数相等，整数与整数之间使用空格隔开。 

【输出格式】 输出到标准输出。包含一行，包括最大值、中位数以及最小值共三个数，并按照从大到小的顺序输出。数据与数据之间使用空格隔开。对于整数请直接输出整数，对于可能出现的分数，请输出四舍五入保留1位小数的结果。

【样例1输入】

```
3
-1 2 4
```

【样例1输出】

```
4 2 -1
```

【样例1解释】 4为最大值，2为中位数，-1为最小值。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;
using ll=long long;
const ll N=1e7+5;

int main()
{
    vector&lt;ll&gt; a;
    ll n,x;
    cin&gt;&gt;n;
    for(ll i=0;i&lt;n;i++)
    {
        cin&gt;&gt;x;
        a.push_back(x);
    }
    if(n%2==1)
        cout&lt;&lt;max(a[0],a[n-1])&lt;&lt;&apos; &apos;&lt;&lt;a[n/2]&lt;&lt;&apos; &apos;&lt;&lt;min(a[0],a[n-1]);
    else
    {
        if((a[n/2]-a[n/2-1])%2==0)
        {
            cout&lt;&lt;max(a[0],a[n-1])&lt;&lt;&apos; &apos;&lt;&lt;(a[n/2]+a[n/2-1])/2&lt;&lt;&apos; &apos;&lt;&lt;min(a[0],a[n-1]);
        }

        else
        {
            cout&lt;&lt;max(a[0],a[n-1])&lt;&lt;&apos; &apos;&lt;&lt;fixed&lt;&lt;setprecision(1)&lt;&lt;(a[n/2]+a[n/2-1])/2.0&lt;&lt;&apos; &apos;&lt;&lt;min(a[0],a[n-1]);
        }
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 201903-2 二十四点 C++ 100</title><link>https://kaitolab.net/blog/csp-201903-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201903-2/</guid><description>CSP认证2019年3月第二题「二十四点」C++满分题解，含运算符优先级优化</description><pubDate>Thu, 29 Dec 2022 16:00:00 GMT</pubDate><content:encoded># 解决方案

博主之前定义的变量为`char a,b;`，实际应该是`int a,b;` 计算的时候变成了ASCII码进行运算，导致部分结果出错，但我codeblock编译器没有任何报错。 此外，我对运算符优先级判断部分进行了小小优化。 2022/6/22更新：

## 修改代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

int isprior(char a, char b) {
    if (a == &apos;x&apos;  a == &apos;/&apos;)
        return 1;
    else if (b==&apos;+&apos;b==&apos;-&apos;)
        return 1;
    else
        return 0;
}

int main()
{
    stack&lt;char&gt; s;//符号
    stack&lt;int&gt; p;//数值
    int n,ans;
    string str;
    char top;
    int a,b;//之前错误代码为 char a,b;

    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;str;
        for(char c:str)
        {
            if(c&gt;=&apos;0&apos;&amp;&amp;c&lt;=&apos;9&apos;)
                p.push(c-48);
            else
            {
                while(!s.empty())
                {
                    top=s.top();
                    //栈顶符号优先级高就出栈进行运算，否则新的运算符入栈
                    if(isprior(top,c))
                    {
                        s.pop();
                        b=p.top();
                        p.pop();
                        a=p.top();
                        p.pop();
                        if(top==&apos;x&apos;)
                            p.push(a*b);
                        else if(top==&apos;/&apos;)
                            p.push(a/b);
                        else if(top==&apos;+&apos;)
                            p.push(a+b);
                        else
                            p.push(a-b);
                    }
                    else
                        break;
                }
                s.push(c);
            }
        }
        //栈中剩余符号继续运算
        while(!s.empty())
        {
            top=s.top();
            s.pop();
            b=p.top();
            p.pop();
            a=p.top();
            p.pop();
            if(top==&apos;x&apos;)
                p.push(a*b);
            else if(top==&apos;/&apos;)
                p.push(a/b);
            else if(top==&apos;+&apos;)
                p.push(a+b);
            else
                p.push(a-b);
        }

        ans=p.top();
        p.pop();
        if(ans==24)
            cout&lt;&lt;&quot;Yes&quot;&lt;&lt;endl;
        else
            cout&lt;&lt;&quot;No&quot;&lt;&lt;endl;
    }
    return 0;
}
```

————————————————————————————— 以下为2022/6/18的原帖内容。

# 原始问题

## 提交结果

我的测试样例全部通过了但是只有70分，求大佬帮我看看哪里出了问题 ![在这里插入图片描述](csp-201903-2/post-1-63afa03888e32.png)

## 题目描述

![在这里插入图片描述](csp-201903-2/post-1-63afa039244e0.png)

## 样例输入

```
10
9+3+4x3
5+4x5x5
7-9-9+8
5x6/5x4
3+5+7+9
1x1+9-9
1x9-5/9
8/5+6x9
6x7-3x6
6x4+4/5
```

## 样例输出

```
Yes
No
No
Yes
Yes
No
No
No
Yes
Yes
```

## 错误代码

我用了两个栈实现，一个存放数值，一个存放符号。

```c
#include&lt;bits/stdc++.h&gt;
using namespace std;

int main()
{
    stack&lt;char&gt; s;//符号
    stack&lt;int&gt; p;//数值
    int n,ans;
    string str;
    char top,a,b;
    map&lt;char,int&gt; dict;

    int priority[4][4]={
        {1,1,0,0},
        {1,1,0,0},
        {1,1,1,1},
        {1,1,1,1}
    };
    dict[&apos;+&apos;]=0;
    dict[&apos;-&apos;]=1;
    dict[&apos;x&apos;]=2;
    dict[&apos;/&apos;]=3;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;str;
        for(char c:str)
        {
            if(c&gt;=&apos;0&apos;&amp;&amp;c&lt;=&apos;9&apos;)
                p.push(c-48);
            else
            {
                while(!s.empty())
                {
                    top=s.top();
                    //栈顶符号优先级高，出栈进行运算，否则新的运算符入栈
                    if(priority[dict[top]][dict[c]])
                    {
                        s.pop();
                        b=p.top();
                        p.pop();
                        a=p.top();
                        p.pop();
                        if(top==&apos;x&apos;)
                            p.push(a*b);
                        else if(top==&apos;/&apos;)
                            p.push(a/b);
                        else if(top==&apos;+&apos;)
                            p.push(a+b);
                        else
                            p.push(a-b);
                    }
                    else
                        break;
                }
                s.push(c);
            }
        }
        //栈中剩余符号继续运算
        while(!s.empty())
        {
            top=s.top();
            s.pop();
            b=p.top();
            p.pop();
            a=p.top();
            p.pop();
            if(top==&apos;x&apos;)
                p.push(a*b);
            else if(top==&apos;/&apos;)
                p.push(a/b);
            else if(top==&apos;+&apos;)
                p.push(a+b);
            else
                p.push(a-b);
        }

        ans=p.top();
        p.pop();
        if(ans==24)
            cout&lt;&lt;&quot;Yes&quot;&lt;&lt;endl;
        else
            cout&lt;&lt;&quot;No&quot;&lt;&lt;endl;
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 201909-1小明种苹果 C++ 100</title><link>https://kaitolab.net/blog/csp-201909-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201909-1/</guid><description>CSP认证2019年9月第一题「小明种苹果」C++满分题解与思路分析</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded/></item><item><title>CSP 201909-2小明种苹果（续） C++ 100</title><link>https://kaitolab.net/blog/csp-201909-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201909-2/</guid><description>【题目描述】 小明在他的果园里种了一些苹果树，这些苹果树排列成一个圆。为了保证苹果的品质，在种植过程中要进行疏果操作。为了更及时地完成疏果操作，小明会不时地检查每棵树的状态，根据需要进行疏果。检查时，如果发现可能有苹果从树上掉落，小明...</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 小明在他的果园里种了一些苹果树，这些苹果树排列成一个圆。为了保证苹果的品质，在种植过程中要进行疏果操作。为了更及时地完成疏果操作，小明会不时地检查每棵树的状态，根据需要进行疏果。检查时，如果发现可能有苹果从树上掉落，小明会重新统计树上的苹果个数（然后根据之前的记录就可以判断是否有苹果掉落了）。在全部操作结束后，请帮助小明统计相关的信息。 

【输入格式】 从标准输入读入数据。 第1行包含一个正整数N,表示苹果树的棵数。 第1+i行(1≤i≤N),每行的格式为m,a1,a2,,am。其中，第一个正整数m表示本行后面的整数个数。后续的mi个整数表示小明对第i棵苹果树的操作记录。若(1≤j≤m)为正整数，则表示小明进行了重新统计该棵树上的苹果个数的操作，统计的苹果个数为aij:若为零或负整数，则表示一次疏果操作，去掉的苹果个数是aij。输入保证一定是正确的，满足： 1.a1&gt;0,即对于每棵树的记录，第一个操作一定是统计苹果个数（初始状态，此时不用判断是否有苹果掉落)： 2.每次疏果操作保证操作后树上的苹果个数仍为正。 

【输出格式】 输出到标准输出。 输出只有一行，包含三个整数T、D、E。其中， ·T为全部疏果操作结束后所有苹果树上剩下的苹果总数（假设每棵苹果树在最后一次统计苹果个数操作后苹果不会因为疏果以外的原因减少)： ·D为发生苹果掉落的苹果树的棵数： ·E为相邻连续三棵树发生苹果掉落情况的组数。 对于第三个统计量的解释：N棵苹果树A1,A2,,Aw排列成一个圆，那么A1与A2相邻，A2与A3相邻，...，AN-1与Aw相邻，Aw与A1相邻。如果A-1,A,A+1这三棵树都发生了苹果掉落的情况，则记为一组。形式化的，有E=(AlDrop(Pred(A))A Drop(A A Drop(Succ(A )))其中，Drop(A)表示苹果树A:是否发生苹果掉落的情况，PrCd(A)表示A:的前一棵树A:-1(如果i&gt;1)或者Aw(如果i=1),Succ(A)表示A:的后一棵树A+1(如果i&lt;N)或者A(如果i=N)。

【样例1输入】

```
4
4 74 -7 -12 -5
5 73 -8 -6 59 -4
5 76 -5 -10 60 -2
5 80 -6 -15 59 0
```

【样例1输出】

```
222 1 0
```

【样例1解释】 全部操作结束后，第1棵树上剩下的苹果个数为74-7-12-5=50，第2棵为59-4=55,第3棵为60-2=58，第4棵为59-0=59。因此T=50+55+58+59=222。 其中，第3棵树在第2次统计之前剩下的苹果个数为76-5-10=61&gt;60，因此发生了苹果掉落的情况。可以检验其他的树没有这种情况，因此D=1。 没有连续三棵树都发生苹果掉落的情况，因此E=0。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;
const int N=1e3+5;

struct appletree
{
    int num;//the number of apple
    int drop=0;//drop=1:drop  drop=0:not drop

}a[N];

int main()
{
    int n,m,tmp;
    int T=0,D=0,E=0;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;m&gt;&gt;a[i].num;
        for(int j=1;j&lt;m;j++)
        {
            cin&gt;&gt;tmp;
            if(tmp&gt;0&amp;&amp;tmp&lt;a[i].num)
            {
                a[i].drop=1;
                a[i].num=tmp;
            }
            else if(tmp&lt;0)
            {
                a[i].num+=tmp;
            }
        }
        T+=a[i].num;
        D+=a[i].drop;
    }
    for(int i=0;i&lt;n;i++)
    {
        if(a[(i+n-1)%n].drop+a[i].drop+a[(i+1)%n].drop==3)
            E++;
    }

    cout&lt;&lt;T&lt;&lt;&apos; &apos;&lt;&lt;D&lt;&lt;&apos; &apos;&lt;&lt;E&lt;&lt;endl;

    return 0;
}
```</content:encoded></item><item><title>CSP 201912-1 报数 C++ 100</title><link>https://kaitolab.net/blog/csp-201912-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201912-1/</guid><description>【题目描述】 甲乙丙丁决定玩一个报数的游戏来打发时间。游戏规则为四个人从1开始轮流进 行报数，但如果需要报出的数是7的倍数或含有数字7则直接跳过。 此外大家约定，在总共报出了个数后（不计入被跳过的数）游戏结束。现在需要 你来帮忙统计，...</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 甲乙丙丁决定玩一个报数的游戏来打发时间。游戏规则为四个人从1开始轮流进 行报数，但如果需要报出的数是7的倍数或含有数字7则直接跳过。 此外大家约定，在总共报出了个数后（不计入被跳过的数）游戏结束。现在需要 你来帮忙统计，游戏过程中每个人各自跳过了几次。 

【输入格式】 从标准输入读入数据。 输入仅一行，包含一个正整数，表示报出了多少个数后游戏结束。 

【输出格式】 输出到标准输出。 输出共四行，每行一个整数，依次表示甲乙丙丁四人在游戏过程中跳过的次数。 

【样例1输入】

```
20
```

【样例1输出】

```
2
1
1
0
```

【样例1解释】 报数过程为： 甲：1，乙：2，丙：3，丁：4 甲：5，乙：6，丙：跳过，丁：8 甲：9，乙：10，丙：11，丁：12 甲：13，乙：跳过，丙：15，丁：16 甲：跳过，乙：18，丙：19，丁：20 甲：跳过，乙：22，丙：23，丁：24 在丁报出24后，四个人总计报出了20个数，游戏结束。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

using ll = long long;

int ifhave7(ll x)
{
    while(x&gt;0)
    {
        if(x%10==7)
            return 1;
        else
            x=x/10;
    }
    return 0;
}

int main()
{
    ll A[4]={0};
    ll n;
    ll i=1,j=0,cnt=0;

    cin&gt;&gt;n;
    while(cnt&lt;n)
    {
        if(i%7==0ifhave7(i))
            A[j]++;
        else
            cnt++;
        i++;
        j=(j+1)%4;
    }
    for(j=0;j&lt;4;j++)
        cout&lt;&lt;A[j]&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 201912-2回收站选址 C++ 100</title><link>https://kaitolab.net/blog/csp-201912-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-201912-2/</guid><description>【题目背景】 开学了，可是校园里堆积了不少垃圾杂物。热心的同学们纷纷自发前来清理，为学校注入正能量~</description><pubDate>Fri, 30 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 开学了，可是校园里堆积了不少垃圾杂物。热心的同学们纷纷自发前来清理，为学校注入正能量~ 

【题目描述】 通过无人机航拍我们己经知晓了n处尚待清理的垃圾位置，其中第i(1≤i≤n)处的坐标为(xi，yi),保证所有的坐标均为整数。我们希望在垃圾集中的地方建立些回收站。具体来说，对于一个位置(x,y)是否适合建立回收站，我们主要考虑以下几点： ·(x,y)必须是整数坐标，且该处存在垃圾： ·上下左右四个邻居位置，即(xy+1)、(K,y-1)、(x+1,y)和(x-1,)处，必须全部存在垃圾： ·进一步地，我们会对满足上述两个条件的选址进行评分，分数为不大于4的自然数，表示在(x±1，y±1)四个对角位置中有几处存在垃圾。 现在，请你统计一下每种得分的选址个数。 

【输入格式】 从标准输入读入数据。 输入总共有n+1行。 第1行包含一个正整数n,表示己查明的垃圾点个数。 第1+i行(1≤i≤n)包含由一个空格分隔的两个整数：和，表示第i处垃圾的坐标。 保证输入的n个坐标互不相同。 

【输出格式】 输出到标准输出。 输出共五行，每行一个整数，依次表示得分为0、1、2、3和4的回收站选址个数。 

【样例1输入】

```
7
12
21
00
11
18
20
01
```

【样例1输出】

```
9
0
1
8
0
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

const int N=300+5;

struct pos
{
    int x,y;
};

int main()
{
    int n;
    int a[5]={0};
    vector&lt;pos&gt; p;
    pos q;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;q.x&gt;&gt;q.y;
        p.push_back(q);
    }

    for(int k=0;k&lt;n;k++)
    {
        int neighbors=0,cnt=0;
        for(int i=0;i&lt;n;i++)
        {
            if(p[i].x==p[k].x-1&amp;&amp;p[i].y==p[k].y)
                neighbors++;
            else if(p[i].x==p[k].x&amp;&amp;p[i].y==p[k].y-1)
                neighbors++;
            else if(p[i].x==p[k].x&amp;&amp;p[i].y==p[k].y+1)
                neighbors++;
            else if(p[i].x==p[k].x+1&amp;&amp;p[i].y==p[k].y)
                neighbors++;
            else if(p[i].x==p[k].x-1&amp;&amp;p[i].y==p[k].y-1)
                cnt++;
            else if(p[i].x==p[k].x-1&amp;&amp;p[i].y==p[k].y+1)
                cnt++;
            else if(p[i].x==p[k].x+1&amp;&amp;p[i].y==p[k].y-1)
                cnt++;
            else if(p[i].x==p[k].x+1&amp;&amp;p[i].y==p[k].y+1)
                cnt++;
        }
        if(neighbors==4)
            a[cnt]++;
    }
    for(int i=0;i&lt;5;i++)
    {
        cout&lt;&lt;a[i]&lt;&lt;endl;
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 202006-1 线性分类器 C++ 100</title><link>https://kaitolab.net/blog/csp-202006-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202006-1/</guid><description>【题目描述】 考虑一个简单的二分类问题——将二维平面上的点分为A和B两类。 训练数据包含n个点，其中第i个点(1≤i≤n)可以表示为一个三元组(x,y,type),即该点的横坐标、纵坐标和类别。 在二维平面上，任意一条直线可以表示为a...</description><pubDate>Sat, 31 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 考虑一个简单的二分类问题——将二维平面上的点分为A和B两类。 训练数据包含n个点，其中第i个点(1≤i≤n)可以表示为一个三元组(x,y,type),即该点的横坐标、纵坐标和类别。 在二维平面上，任意一条直线可以表示为a+bx+cy=0的形式，即由a、b和c三个参数确定该直线，且满足b、c不同时为0。 基于这个已知类别的点，我们想要在平面上找到一条直线作为一个线性分类器。 具体来说，这条线要把训练数据中的A、B两类点完美分隔开来，即一侧只有A类点、另一侧只有B类点。这样，对于任意一个的未知类别的点，我们就可以根据它是位于直线的哪一侧来预测它的类别了。 在本题中我们仅需要处理m个如下查询：给定一条直线，判断它是否能将训练数据中的A、B两类点完美分开。 

【输入格式】 从标准输入读入数据。 输入共n+m+1行 第一行包含用空格分隔的两个正整数和m,分别表示点和查询的个数。 第二行到第n+1行依次输入n个点的信息。第i+1行（1≤i≤n)包含用空格 分隔的三项x、y和type,分别表示第i个点的横、纵坐标和类别，其中坐标为整数、类别为一个大写英文字母A或B。 第n+2行到第n+m+1行依次输入m个查询。第j+n+1行(1≤j≤m)包含用空格分隔的三个整数a，b和c，表示第J个查询中给定直线的三个参数。 

【输出格式】 输出到标准输出。 输出共m行，每行输出一个字符串. 第j行(1≤j≤m)输出的字符串对应第j个查询的结果：如果给定直线可以完美分隔A、B两类点，则输出Yes:否则输出No。 

【样例1输入】

```
9 3
1 1 A
1 0 A
1 -1 A
2 2 B
2 3 B
0 1 A
3 1 B
1 3 B
2 0 A
0 2 -3
-3 0 2
-3 1 1
```

【样例1输出】

```
NO
NO
YES
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

const int N=1000;
struct node
{
    int x;
    int y;
    char type;
}a[N];

bool cmp(node a,node b)
{
    return a.type&lt;b.type;
}

int main()
{
    int n,m;
    int theta0,theta1,theta2,pre,result,flag=1;
    cin&gt;&gt;n&gt;&gt;m;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;a[i].x&gt;&gt;a[i].y&gt;&gt;a[i].type;
    }
    sort(a,a+n,cmp);

    for(int i=0;i&lt;m;i++)
    {
        cin&gt;&gt;theta0&gt;&gt;theta1&gt;&gt;theta2;
        for(int j=0;j&lt;n;j++)
        {
            result=(theta0+theta1*a[j].x+theta2*a[j].y&gt;0)?1:0;
            if(j==0)
                flag=1;
            else if((result!=pre&amp;&amp;a[j].type==a[j-1].type)(result==pre&amp;&amp;a[j].type!=a[j-1].type))
            {
                flag=0;
            }
            pre=result;
        }
        if(flag)
            cout&lt;&lt;&quot;Yes&quot;&lt;&lt;endl;
        else
            cout&lt;&lt;&quot;No&quot;&lt;&lt;endl;
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 202006-2 稀疏向量 C++ 100</title><link>https://kaitolab.net/blog/csp-202006-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202006-2/</guid><description>【题目描述】 对于一个n维整数向量v∈Z”，其在第index个维度上的取值记作Vindex。这里我们约定index的取值从1开始，即v=(v1,v2…,vn)。下面介绍一种向量的稀疏表示方法。 如果v仅在少量维度上的取值不为0，则称其...</description><pubDate>Sat, 31 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 对于一个n维整数向量v∈Z”，其在第index个维度上的取值记作Vindex。这里我们约定index的取值从1开始，即v=(v1,v2…,vn)。下面介绍一种向量的稀疏表示方法。 如果v仅在少量维度上的取值不为0，则称其为稀疏向量。 例如当=10时，v=(0,0,0,5,0,0,-3,0,0,1)就是一个稀疏向量。 由于稀疏向量的非零值较少，我们可以通过仅存储非零值的方式来节省空间。具体来说，每个非零值都可以用一个(index,value)对来表示，即该向量在第index个维度上的取值Vindex=value。在上面的例子中，v就可以表示为\[(4,5)，(7，-3)，(10,1)\]。 接下来给出这种稀疏表示一般化的定义。 ·对于任意一个n维整数向量v∈Z”，如果其在且仅在a个维度上取值不为0，则 可以唯一表示为：\[(index1,valve1),(index2,valve2),···(indexa,valuea)\] ·其中所有的index均为整数且满足： 1≤index1&lt;indew2&lt;···&lt;indexa≤n ·valuei:表示问量v在对应维度indexi上的非零值。 给出两个n维整数问量u,v∈Z”的稀疏表示，试计算它们的内积。 

【输入格式】 从标准输入读入数据。 输入的第一行包含用空格分隔的三个正整数n、a和b，其中n表示向量u、v的 维数，a和b分别表示两个向量所含非零值的个数。 第二行到第a+1行输入向量的稀疏表示。第i+1行(1≤i≤a)包含用空格 分隔的两个整数indexi和valuei,表示Vindexj=valuej 第a+2行到第a+b+1行输入向量v的稀疏表示。第j+a+1行(1≤j≤b) 包含用空格分隔的两个整数indexj;和valuej,表示Vindexj=valuej 

【输出格式】 输出到标准输出。 输出一个整数，表示向量u和v的内积u·v。 

【样例输入】

```
10 3 4
4 5
7 -3
10 1
1 10
4 20
5 30
7 40
```

【样例输出】

```
-20
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

using ll = long long;
const ll N=5e5+5;

struct vec
{
    int index;
    int value;
}A[N],B[N];

int main()
{
    ll n,a,b;
    ll i,j;
    ll sum=0;
//    ifstream cin(&quot;test.in&quot;);
//    ofstream cout(&quot;test.out&quot;);
    cin&gt;&gt;n&gt;&gt;a&gt;&gt;b;
    for(ll i=0;i&lt;a;i++)
    {
        cin&gt;&gt;A[i].index&gt;&gt;A[i].value;
    }
    for(ll i=0;i&lt;b;i++)
    {
        cin&gt;&gt;B[i].index&gt;&gt;B[i].value;
    }
    i=j=0;
    while(i&lt;a&amp;&amp;j&lt;b)
    {
        if(A[i].index==B[j].index)
        {
            sum+=A[i].value*B[j].value;
            i++;
            j++;
        }
        else if(A[i].index&lt;B[j].index)
            i++;
        else
            j++;
    }
    cout&lt;&lt;sum&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202009-1 称检测点查询 C++ 100</title><link>https://kaitolab.net/blog/csp-202009-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202009-1/</guid><description>【题目背景】 2020年6月8日，国务院联防联控机制发布《关于加快推进新冠病毒核酸检测的实施意见》，提出对“密切接触者”等八类重点人群“应检尽检”，其他人群“愿检尽检”。</description><pubDate>Sat, 31 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 2020年6月8日，国务院联防联控机制发布《关于加快推进新冠病毒核酸检测的实施意见》，提出对“密切接触者”等八类重点人群“应检尽检”，其他人群“愿检尽检”。 

【问题描述】 某市设有n个核酸检测点，编号从1到n,其中i号检测点的位置可以表示为一个平面整数坐标(xi,yi) 为方便预约核酸检测，请根据市民所在位置(X,Y),查询距其最近的三个检测点。 多个检测点距离相同时，编号较小的视为更近。 

【输入格式】 输入共n+1行。 第一行包含用空格分隔的三个整数n、X和Y,表示检测点总数和市民所在位置。 第二行到第n+1行依次输入n个检测点的坐标。第i+1行(1≤i≤n)包含用空格分隔的两个整数：xi和yi,表示 i号检测点所在位置。 

【输出格式】 输出共三行，按距离从近到远，依次输出距离该市民最近的三个检测点编号。 

【样例输入1】

```
3 2 2
2 2
2 3
2 4
```

【样例输出1】

```
1
2
3
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

const int N=205;

struct dist
{
    int id;
    double d;
}a[N];

bool cmp(dist A,dist B)
{
    if(A.d==B.d)
        return A.id&lt;B.id;
    return A.d&lt;B.d;
}

int main()
{
    int n,X,Y,xi,yi;
    cin&gt;&gt;n&gt;&gt;X&gt;&gt;Y;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;xi&gt;&gt;yi;
        a[i].id=i+1;
        a[i].d=pow(xi-X,2)+pow(yi-Y,2);
    }
    sort(a,a+n,cmp);
    cout&lt;&lt;a[0].id&lt;&lt;endl&lt;&lt;a[1].id&lt;&lt;endl&lt;&lt;a[2].id&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202009-2 风险人群筛查 C++ 100</title><link>https://kaitolab.net/blog/csp-202009-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202009-2/</guid><description>【题目背景】 某地疫情爆发后，出于“应检尽检”的原则，我们想要通知所有近期经过该高危区域的居民参与核酸检测。</description><pubDate>Sat, 31 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 某地疫情爆发后，出于“应检尽检”的原则，我们想要通知所有近期经过该高危区域的居民参与核酸检测。

【问题描述】 想要找出经过高危区域的居民，分析位置记录是一种简单有效的方法。

具体来说，一位居民的位置记录包含 t个平面坐标(x1,y1),(x2,y2),···,(xt,yt) ，其中(xi,yi表示该居民i时刻所在位置。 高危区域则可以抽象为一个矩形区域（含边界），左下角和右上角的坐标分别为(xl,yd)和(xr,yu)，满足xl&lt;xr且yd&lt;yu 。

考虑某位居民的位置记录，如果其中某个坐标位于矩形内（含边界），则说明该居民经过高危区域；进一步地，如果其中连续k个或更多坐标均位于矩形内（含边界），则认为该居民曾在高危区域逗留。需要注意的是，判定经过和逗留时我们只关心位置记录中的t个坐标，而无需考虑该居民在i到i+1时刻之间位于何处。

给定高危区域的范围和n位居民过去 t个时刻的位置记录，试统计其中经过高危区域的人数和曾在高危区域逗留的人数。

【输入格式】 输入共n+1行。

第一行包含用空格分隔的七个整数 n、k、t、xi、td、xr和yu ，含义如上文所述。

接下来n行，每行包含用空格分隔的2t个整数，按顺序表示一位居民过去 t个时刻的位置记录(x1,y1),(x2,y2),···,(xt,yt) 。

【输出格式】 输出共两行，每行一个整数，分别表示经过高危区域的人数和曾在高危区域逗留的人数。

【样例输入1】

```
5 2 6 20 40 100 80
100 80 100 80 100 80 100 80 100 80 100 80
60 50 60 46 60 42 60 38 60 34 60 30
10 60 14 62 18 66 22 74 26 86 30 100
90 31 94 35 98 39 102 43 106 47 110 51
0 20 4 20 8 20 12 20 16 20 20 20
```

【样例输出1】

```
3
2
```

【样例1说明】 如下图红色标记所示，前三条位置记录经过了高危区域； 但第三条位置记录（图中左上曲线）只有一个时刻位于高危区域内，不满足逗留条件。

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

int main()
{
    int a[20]={0};
    int n,k,t,xl,yd,xr,yu,x,y;
    int pass=0,stop=0,tmax=0,flag=0;
    cin&gt;&gt;n&gt;&gt;k&gt;&gt;t&gt;&gt;xl&gt;&gt;yd&gt;&gt;xr&gt;&gt;yu;
    for(int i=0;i&lt;n;i++)
    {
        tmax=0;
        flag=0;
        for(int j=0;j&lt;t;j++)
        {
            cin&gt;&gt;x&gt;&gt;y;
            if(x&gt;=xl&amp;&amp;x&lt;=xr&amp;&amp;y&gt;=yd&amp;&amp;y&lt;=yu)
            {
                if(flag==0)
                {
                    a[i]=max(a[i],tmax);
                    tmax=1;
                    flag=1;
                }
                else
                    tmax++;
            }
            else
                flag=0;
        }
        a[i]=max(a[i],tmax);
    }
    for(int i=0;i&lt;n;i++)
    {
        if(a[i]&gt;=k)
        {
            pass++;
            stop++;
        }
        else if(a[i]&gt;0)
        {
            pass++;
        }
    }
    cout&lt;&lt;pass&lt;&lt;endl&lt;&lt;stop&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202012-1 期末预测之安全指数 C++ 100</title><link>https://kaitolab.net/blog/csp-202012-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202012-1/</guid><description>【题目背景】 期末要到了，小菜同学找到了自己的好朋友顿顿，希望可以预测一下自己这学期是否会挂科。</description><pubDate>Sat, 31 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 期末要到了，小菜同学找到了自己的好朋友顿顿，希望可以预测一下自己这学期是否会挂科。

【题目描述】 首先，顿顿选取了如“课堂表现”、“自习时长”、“作业得分”、“社团活动参与度”等n项指标作为预测的依据。 然后，顿顿根据自己平日里对小菜的暗中观察，以百分制给每一项指标打分，即小菜同学第i(1≤i≤n)项指标的得分scorei是一个\[0,100\]范围内的整数。 鉴于每一项指标的重要性不尽相同，顿顿用一个\[-10,10\]范围内的整数 来表示第i(1≤i≤n)项指标的重要程度。 最后，小菜同学期末的安全指数y定义如下： y=ReLU(Escorei·wi) 如果安全指数过低（甚至为零），则说明小菜同学这学期很可能要挂科了…… 已知每一项指标的重要程度wi和相应的得分scorei，快来算算小菜同学期末的安全指数吧。

【输入格式】 从标准输入读入数据。 输入的第一行包含一个正整数n，保证2≤n≤10^5。 接下来输入n行，其中第i(1≤i≤n)行包含用空格分隔的两个整数wi和scorei，分别表示第i项指标的重要程度和小菜同学该项的得分。

【输出格式】 输出到标准输出。 输出一个非负整数y，表示小菜同学期末的安全指数。

【样例1输入】

```
6
2 60
10 100
0 70
0 0
-10 50
10 60
```

【样例1输出】

```
1220
```

【样例1解释】 y=ReLU(1220)=1220

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;
using namespace std;

int main()
{
    int n,w,score,total=0;
    cin&gt;&gt;n;
    while(n--)
    {
        cin&gt;&gt;w&gt;&gt;score;
        total+=w*score;
    }
    cout&lt;&lt;max(0,total)&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202012-2期末预测之最佳阈值 C++ 100</title><link>https://kaitolab.net/blog/csp-202012-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202012-2/</guid><description>【题目背景】 考虑到安全指数是一个较大范围内的整数、小菜很可能搞不清楚自己是否真的安全，顿顿决定设置一个阈值 ，以便将安全指数y转化为一个具体的预测结果——“会挂科”或“不会挂科”。 因为安全指数越高表明小菜同学挂科的可能性越低，所以...</description><pubDate>Sat, 31 Dec 2022 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 考虑到安全指数是一个较大范围内的整数、小菜很可能搞不清楚自己是否真的安全，顿顿决定设置一个阈值 ，以便将安全指数y转化为一个具体的预测结果——“会挂科”或“不会挂科”。 因为安全指数越高表明小菜同学挂科的可能性越低，所以当$y≥\theta$时，顿顿会预测小菜这学期很安全、不会挂科；反之若$y&lt;\theta$，顿顿就会劝诫小菜：“你期末要挂科了，勿谓言之不预也。” 那么这个阈值该如何设定呢？顿顿准备从过往中寻找答案。

【题目描述】 具体来说，顿顿评估了m位同学上学期的安全指数，其中第 $i(1\leq i\leq m)$位同学的安全指数为yi，是一个\[0,10^8\]范围内的整数；同时，该同学上学期的挂科情况记作$result_i\in 0,1$，其中0表示挂科、1表示未挂科。 相应地，顿顿用$predict_\theta(y)$表示根据阈值$\theta$将安全指数 y 转化为的具体预测结果。 如果$predict_\theta(y_j)$与 $result_j$相同，则说明阈值为$\theta$时顿顿对第 j 位同学是否挂科预测正确；不同则说明预测错误。

$$
{predict}_\theta\left(y\right)=\left\{
\begin{aligned}
0 \qquad (y&lt;θ)\\
1 \qquad (y≥θ)
\end{aligned}
\right.
$$

最后，顿顿设计了如下公式来计算最佳阈值$\theta^*$:

$$
\theta^*={\underset{\theta\in y_i}{\arg\max}\sum_{j=1}^{m}(predict_\theta(y_i)==result_j)}
$$

该公式亦可等价地表述为如下规则： 1.最佳阈值仅在yi 中选取，即与某位同学的安全指数相同； 2.按照该阈值对这 m 位同学上学期的挂科情况进行预测，预测正确的次数最多（即准确率最高）； 3.多个阈值均可以达到最高准确率时，选取其中最大的。

【输入格式】 从标准输入读入数据。 输入的第一行包含一个正整数 。 接下来输入m行，其中第$i(1\leq i\leq m)$行包括用空格分隔的两个整数 yi和resulti，含义如上文所述。

【输出格式】 输出到标准输出。 输出一个整数，表示最佳阈值。

【样例1输入】

```
6
0 0
1 0
1 1
3 1
5 1
7 1
```

【样例1输出】

```
3
```

【样例1解释】 按照规则一，最佳阈值的选取范围为0,1,3,5,7。 阈值为0时，预测正确次数为 4； 阈值为1时，预测正确次数为 5； 阈值为3时，预测正确次数为 5； 阈值为5时，预测正确次数为 4； 阈值为7时，预测正确次数为 3。 阈值选取为 1 或 3 时，预测准确率最高； 所以按照规则二，最佳阈值的选取范围缩小为1,3。 依规则三，$\theta^*=max1,3=3$。

## 代码

```cpp
//前缀和
#include&lt;bits/stdc++.h&gt;
using namespace std;

const int N=1e5+5;
struct node {
    int y;
    int result;
}a[N];

bool cmp(node A,node B)
{
    return (A.y&lt;B.y);
}

int main()
{
    int m,t,j=0,num=0,cnt=0,ans=0;
    int b[N]={0};
    cin&gt;&gt;m;
    for(int i=0;i&lt;m;i++)
    {
        cin&gt;&gt;a[i].y&gt;&gt;a[i].result;
    }
    sort(a,a+m,cmp);
    b[0]=a[0].result;
    for(int i=1;i&lt;m;i++)
    {
        b[i]=b[i-1]+a[i].result;
    }
    for(int i=0;i&lt;m;i++)
    {
        if(i==0)
        {
            t=0;
            j=-1;
        }
        else
        {
            j=i-1;
            while(a[j].y==a[i].y){j--;}
            t=b[j];
        }
        num=j+1-t+b[m-1]-t;
        //cout&lt;&lt;a[i].y&lt;&lt;&apos; &apos;&lt;&lt;num&lt;&lt;endl;
        if(num&gt;=cnt)
        {
            cnt=num;
            ans=a[i].y;
        }
    }
    cout&lt;&lt;ans&lt;&lt;endl;
    return 0;

}
```</content:encoded></item><item><title>CSP 202104-1 灰度直方图 C++ 100</title><link>https://kaitolab.net/blog/csp-202104-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202104-1/</guid><description>【题目描述】 一幅长宽分别为n个像素和m个像素的灰度图像可以表示为一个n×m大小的矩阵A。 其中每个元素$A_{ij}$(0≤i&lt;n、0≤j&lt;m) 是一个\0，L)范围内的整数，表示对应位置像素的灰度值。 具体来说，一个8比特的灰度图...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 一幅长宽分别为n个像素和m个像素的灰度图像可以表示为一个n×m大小的矩阵A。 其中每个元素$A_{ij}$(0≤i&lt;n、0≤j&lt;m) 是一个\[0，L)范围内的整数，表示对应位置像素的灰度值。 具体来说，一个8比特的灰度图像中每个像素的灰度范围是\[0,128)。 一副灰度图像的灰度统计直方图（以下简称“直方图”）可以表示为一个长度为L的数组h,其中$h[x]$(0≤x&lt;L)表示该图像中灰度值为x的像素个数。显然，h\[0\]到h\[L-1\]的总和应等于图像中的像素总数n·m。 已知一副图像的灰度矩阵A,试计算其灰度直方图h\[0\],h\[1\],···,h\[L-1\]。

【输入格式】 输入共n+1行。 输入的第一行包含三个用空格分隔的正整数n、m和L,含义如前文所述。 第二到第n+1行输入矩阵A。 第i+2(0≤i&lt;n)行包含用空格分隔的m个整数，依次为$A_{i0},A_{i1},···,A_{i(m-1)}$

【输出格式】 输出仅一行，包含用空格分隔的L个整数h\[0\],h\[1\],···,h\[L-1\],表示输入图像的灰度直方图。

【样例输入】

```
4 4 16
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
```

【样例输出】

```
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
```

## 代码

```cpp
#include&lt;bits/stdc++.h&gt;

using namespace std;

int main()
{
    int n,m,L,a;
    int h[256]={0};
    cin&gt;&gt;n&gt;&gt;m&gt;&gt;L;
    for(int i=0;i&lt;n;i++)
    {
        for(int j=0;j&lt;m;j++)
        {
            cin&gt;&gt;a;
            h[a]++;
        }
    }
    for(int i=0;i&lt;L;i++)
    {
        cout&lt;&lt;h[i]&lt;&lt;&apos; &apos;;
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 202104-2 邻域均值 C++ 100</title><link>https://kaitolab.net/blog/csp-202104-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202104-2/</guid><description>【试题背景】 顿顿在学习了数字图像处理后，想要对手上的一副灰度图像进行降噪处理。不过该图像仅在较暗区域有很多噪点，如果贸然对全图进行降噪，会在抹去噪点的同时也模糊了原有图像。因此顿顿打算先使用邻域均值来判断一个像素是否处于较暗区域，然...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【试题背景】 顿顿在学习了数字图像处理后，想要对手上的一副灰度图像进行降噪处理。不过该图像仅在较暗区域有很多噪点，如果贸然对全图进行降噪，会在抹去噪点的同时也模糊了原有图像。因此顿顿打算先使用**邻域均值**来判断一个像素是否处于**较暗区域**，然后仅对处于**较暗区域**的像素进行降噪处理。

【问题描述】 待处理的灰度图像长宽皆为 n 个像素，可以表示为一个 n×n大小的矩阵 A，其中每个元素是一个\[0,L)范围内的整数，表示对应位置像素的灰度值。 对于矩阵中任意一个元素$A_{ij}$(0≤i,j&lt;n) ，其邻域定义为附近若干元素的集和： $111$

这里使用了一个额外的参数 r 来指明$A_{ij}$附近元素的具体范围。根据定义，易知$neighbor(i,j,r)$最多有$(2r+1)^2$个元素。

如果元素$A_{ij}$**邻域**中所有元素的**平均值**小于或等于一个给定的阈值 t，我们就认为该元素对应位置的像素处于较暗区域。 下图给出了两个例子，左侧图像的较暗区域在右侧图像中展示为黑色，其余区域展示为白色。 ![](csp-202104-2/RequireFile-300x197.jpg)

现给定邻域参数 r 和阈值 t，试统计输入灰度图像中有多少像素处于**较暗区域**。

【输入格式】 输入共 n+1 行。

输入的第一行包含四个用空格分隔的正整数 n、L、r 和 t，含义如前文所述。

第二到第 n+1 行输入矩阵 A。 第`i+2`(0≤i&lt;n)行包含用空格分隔的 n 个整数，依次为$A_{i0},A_{i1},···,A_{i(n-1)}$。

【输出格式】 输出一个整数，表示输入灰度图像中处于较暗区域的像素总数。

【样例输入】

```
4 16 1 6
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
```

【样例输出】

```
7
```

【样例输入】

```
11 8 2 2
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 7 0 0 0 7 0 0 7 7 0
7 0 7 0 7 0 7 0 7 0 7
7 0 0 0 7 0 0 0 7 0 7
7 0 0 0 0 7 0 0 7 7 0
7 0 0 0 0 0 7 0 7 0 0
7 0 7 0 7 0 7 0 7 0 0
0 7 0 0 0 7 0 0 7 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
```

【样例输出】

```
83
```

## 代码

```cpp
//二维数组前缀和
#include &lt;bits/stdc++.h&gt;

using namespace std;

const int N = 600 + 1;
int a[N][N], psum[N][N];

int main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    memset(psum, 0, sizeof psum);

    int n,L,r,t,Lsum,cnt=0;
    int row1,row2,col1,col2;
    cin&gt;&gt;n&gt;&gt;L&gt;&gt;r&gt;&gt;t;
    for(int i=1;i&lt;=n;i++)
    {
        for(int j=1;j&lt;=n;j++)
        {
            cin&gt;&gt;a[i][j];
            psum[i][j]=psum[i][j-1]+psum[i-1][j]-psum[i-1][j-1]+a[i][j];
        }
    }
    for(int i=1;i&lt;=n;i++)
    {
        row1=max(0,i-r-1);
        row2=min(n,i+r);
        for(int j=1;j&lt;=n;j++)
        {
            col1=max(0,j-r-1);
            col2=min(n,j+r);
            Lsum=psum[row2][col2]-psum[row1][col2]-psum[row2][col1]+psum[row1][col1];
            //cout&lt;&lt;i&lt;&lt;j&lt;&lt;&apos; &apos;&lt;&lt;Lsum&lt;&lt;&apos; &apos;&lt;&lt;(col2-col1)*(row2-row1)*t&lt;&lt;endl;
            if(Lsum&lt;=(col2-col1)*(row2-row1)*t)
            {

                cnt++;
            }
        }
    }
    cout&lt;&lt;cnt&lt;&lt;endl;
}
```</content:encoded></item><item><title>CSP 202109-1 数组推导 C++ 100</title><link>https://kaitolab.net/blog/csp-202109-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202109-1/</guid><description>【题目描述】</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目
【题目描述】

【输入格式】
从标准输入读入数据。
输入的第一行包含一个正整数 n.
输入的第二行包含 n 个用空格分隔的自然数B1,B2,···,Bn
【输出格式】
输出到标准输出。
输出共两行。
第一行输出一个整数，表示sum的最大值。
第二行输出一个整数，表示sum的最小值。
【样例输入】

```
6
0 0 5 5 10 10
```

【样例输出】
```
30
15
```

## 代码
```cpp
#include&lt;iostream&gt;
using namespace std;

int main()
{
    int sum_max,sum_min;
    int n,pre,cur;
    cin&gt;&gt;n;
    pre=-1;
    sum_max=0;
    sum_min=0;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;cur;
        if(cur!=pre)
        {
            sum_min+=cur;
        }
        sum_max+=cur;
        pre=cur;
    }
    cout&lt;&lt;sum_max&lt;&lt;endl&lt;&lt;sum_min&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202109-2 非零段划分 C++ 100</title><link>https://kaitolab.net/blog/csp-202109-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202109-2/</guid><description>【题目描述】 A1,A2,···,An是一个由n个自然数（非负整数）组成的数组。我们称其中Ai,···,Aj是一个非零段，当且仅当以下条件 同时满足： ·1≤i≤j≤n; ·对于任意的整数k,若i≤k≤j,则$A_k$\0; ·i=1...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 A1,A2,···,An是一个由n个自然数（非负整数）组成的数组。我们称其中Ai,···,Aj是一个非零段，当且仅当以下条件 同时满足： ·1≤i≤j≤n; ·对于任意的整数k,若i≤k≤j,则$A_k$\&gt;0; ·i=1或$A_{i-1}$\=0; ·j=n或$A_{j+1}$\=0。 下面展示了几个简单的例子： ·A=\[3,1,2,0,0,2,0,4,5,0,2\]中的4个非零段依次为\[3,1,2\]、\[2\]、\[4,5\]和\[2\]; ·A=\[2,3,1,4,5\]仅有1个非零段： ·A=\[0,0,0\]则不含非零段（即非零段个数为0）。 现在我们可以对数组A进行如下操作：任选一个正整数p,然后将A中所有小于p的数都变为0。试选取一个合适的P, 使得数组 A 中的非零段个数达到最大。若输入的 A 所含非零段数己达最大值，可取p=1,即不对 A 做任何修改。

【输入格式】 从标准输入读入数据。 输入的第一行包含一个正整数 n。 输入的第二行包含 n 个用空格分隔的自然数A1,A2,···,An。

【输出格式】 输出到标准输出。 仅输出一个整数，表示对数组 A 进行操作后，其非零段个数能达到的最大值。

【样例输入】

```
11
3 1 2 0 0 2 0 4 5 0 2
```

【样例输出】

```
5
```

## 代码

```cpp
#include &lt;bits/stdc++.h&gt;

using namespace std;

const int N=5e5+5;
struct node {
    int x;
    int id;
}a[N];

bool cmp(node A,node B) {
    if(A.x==B.x)
        return A.id&lt;B.id;
    return A.x&lt;B.x;
}

int main()
{
    int n,ans,cnt,pre;
    int b[N]={0};
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)
    {
        a[i].id=i+1;
        cin&gt;&gt;a[i].x;
    }
    sort(a,a+n,cmp);
    b[0]=1;
    b[n+1]=1;
    pre=a[0].x;
    cnt=1;
    ans=0;
    for(int i=0;i&lt;n;i++)
    {
        int id;
        if(a[i].x!=0&amp;&amp;a[i].x!=pre)
            ans=max(ans,cnt);
        id=a[i].id;
        if(b[id-1]==0&amp;&amp;b[id+1]==0)
            cnt++;
        else if(b[id-1]==1&amp;&amp;b[id+1]==1)
            cnt--;
        b[id]=1;
        pre=a[i].x;
    }
    cout&lt;&lt;ans&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202112-1 序列查询 C++ 100</title><link>https://kaitolab.net/blog/csp-202112-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202112-1/</guid><description>【题目背景】 西西艾弗岛的购物中心里店铺林立，商品琳琅满目。为了帮助游客根据自己的预算快速选择心仪的商品，IT部门决定研发一套商品检索系统，支持对任意给定的预算x,查询在该预算范围内（≤x)价格最高的商品。如果没有商品符合该预算要求，...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 西西艾弗岛的购物中心里店铺林立，商品琳琅满目。为了帮助游客根据自己的预算快速选择心仪的商品，IT部门决定研发一套商品检索系统，支持对任意给定的预算x,查询在该预算范围内（≤x)价格最高的商品。如果没有商品符合该预算要求，便向游客推荐可以免费领取的西西艾弗岛定制纪念品。 假设购物中心里有n件商品，价格从低到高依次为A1,A2···An,则根据预算x检索商品的过程可以抽象为如下序列查询问题。

【题目描述】 A=\[A0,A1,A2,···,An\]是一个由n+1个\[0，N)范围内整数组成的序列，满足0=A0&lt;A1&lt;A2&lt;···&lt;An&lt;N。 (这个定义中蕴含了n一定小于N。) 基于序列A,对于\[0，N)范围内任意的整数x,查询f(x)定义为：序列A中小于等于x的整数里最大的数的下标。 具体来说有以下两种情况： 1.存在下标0≤i&lt;n满足Ai≤x&lt;Ai+1 此时序列A中从A0到Ai均小于等于x,其中最大的数为Ai,其下标为i，故f(x)=i。 2.An≤x 此时序列A中所有的数都小于等于x,其中最大的数为An,故f(x)=n。 令sum(A)表示f(0)到f(N-1)的总和，即：

$$
sum(A)=∑f(i)=f(0)+f(1)+f(2)+···+f(N-1)
$$

对于给定的序列A,试计算sum(A)。

【输入格式】 从标准输入读入数据。 输入的第一行包含空格分隔的两个正整数n和N。 输入的第二行包含n个用空格分隔的整数A1,A2,···,An 注意 A0 固定为 0，因此输入数据中不包括A0。

【输出格式】 输出到标准输出。 仅输出一个整数，表示 sum(A) 的值。

【样例输入】

```
3 10
2 5 8
```

【样例输出】

```
15
```

## 代码

```cpp
#include &lt;iostream&gt;

using namespace std;
/*
int fun_x(int x)
{
    int start_i;
    fun_x(x-1)
    for(int i=start_i)
}*/

int main()
{
    int A[201];
    int i,j,N,n,sum,f;

    cin&gt;&gt;n&gt;&gt;N;
    A[0]=0;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;A[i+1];
    }
    i=0;
    j=0;
    f=0;
    sum=0;
    while(i&lt;N)
    {
        if(j+1&lt;=n)
        {
            sum+=(A[j+1]-A[j])*f;
            j++;
            f=j;
            i=A[j];
        }
        else
        {
            sum+=(N-A[j])*f;
            i=N;
        }
    }
    cout&lt;&lt;sum&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202112-2 序列查询新解 C++ 70</title><link>https://kaitolab.net/blog/csp-202112-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202112-2/</guid><description>【题目背景】 上一题“序列查询”中说道： A=\A0,A1,A2,,AN\是一个由n+1个\0，N)范围内整数组成的序列，满足0=A0&lt;A1&lt;A2&lt;···&lt;An&lt;N。基于序列A,对于\O,N)范围内任意的整数x,查询f(x)定义为：序...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 上一题“序列查询”中说道： A=\[A0,A1,A2,,AN\]是一个由n+1个\[0，N)范围内整数组成的序列，满足0=A0&lt;A1&lt;A2&lt;···&lt;An&lt;N。基于序列A,对于\[O,N)范围内任意的整数x,查询f(x)定义为：序列A中小于等于x的整数里最大的数的下标。 对于给定的序列A和整数x,查询f(x)是一个很经典的问题，可以使用二分搜索在O(logn)的时间复杂度内轻松解决。但在IT部门讨论如何实现这一功能时，小P同学提出了些新的想法。

【题目描述】 小P同学认为，如果事先知道了序列A中整数的分布情况，就能直接估计出其中小于等于x的最大整数的大致位置。 接着从这一估计位置开始线性查找，锁定(x)。如果估计得足够准确，线性查找的时间开销可能比二分查找算法更小。 比如说，如果A1,A2,···,An均匀分布在(0，N)的区间，那么就可以估算出：
$$
f(x)≈{(n+1)·x}/{N}
$$

为了方便计算，小P首先定义了比例系数$r=N/{n+1}$，其中表示下取整，即r等于N除以n+1的商。进一步 地，小P用g(x)=x/r表示自己估算出的(x)的大小，这里同样使用了下取整来保证g(x)是一个整数。 显然，对于任意的询问x∈\[O,N),g(x)和f(x)越接近则说明小P的估计越准确，后续进行线性查找的时间开销也越小。因此，小P用两者差的绝对值g(x)一f(x)来表示处理询问x时的误差。 为了整体评估小P同学提出的方法在序列A上的表现，试计算：

$$
error(A)=g(0)-f(0)+···+g(N-1)-f(N-1)
$$

【输入格式】 从标准输入读入数据。 输入的第一行包含空格分隔的两个正整数 n 和 N。 输入的第二行包含 n 个用空格分隔的整数A1,A2,···,An 注意 A0 固定为 0，因此输入数据中不包括A0。 

【输出格式】 输出到标准输出。 仅输出一个整数，表示error(A)的值。

【样例输入】

```
3 10
2 5 8
```

【样例输出】

```
5
```

## 代码

```cpp
#include &lt;iostream&gt;
#include &lt;algorithm&gt;

using namespace std;

int main()
{
    int A[201];
    int B[1000]={0};//f(i)-g(i)
    int i,j,N,n,r,sum;

    cin&gt;&gt;n&gt;&gt;N;
    r=int(N/(n+1));
    A[0]=0;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;A[i+1];
    }
    for(i=0;i&lt;n+1;i++)
    {

        if(i&lt;n)
        {
            B[A[i]]+=i;
            B[A[i+1]]-=i;
        }
        else
        {
            B[A[i]]+=i;
            B[N]-=i;
        }
    }
    j=0;
    for(i=0;i&lt;N;i=i+r)
    {
        if(i+r&lt;N)
        {
            B[i]-=j;
            B[i+r]+=j;
        }
        else
        {
            B[i]-=j;
            B[N]+=j;
        }
        j++;
    }
    sum=0;
    for(i=1;i&lt;N;i++)
    {
        B[i]=B[i]+B[i-1];
        sum+=abs(B[i]);
    }
    cout&lt;&lt;sum&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202203-1 未初始化警告 C++ 100</title><link>https://kaitolab.net/blog/csp-202203-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202203-1/</guid><description>【题目背景】 一个未经初始化的变量，里面存储的值可能是任意的。因此直接使用未初始化的变量，比如将其赋值给另一个变量，并不符合一般的编程逻辑。代码中出现这种情况，往往是因为遗漏了初始化语句、或是打错了变量名。对代码中使用了未初始化变量的...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 一个未经初始化的变量，里面存储的值可能是任意的。因此直接使用未初始化的变量，比如将其赋值给另一个变量，并不符合一般的编程逻辑。代码中出现这种情况，往往是因为遗漏了初始化语句、或是打错了变量名。对代码中使用了未初始化变量的语句进行检查，可以方便地排查出代码中的一些隐秘Bug。 

【问题描述】 考虑一段包含k条赋值语句的简单代码。该段代码最多使用到n个变量，分别记作a1,a2,···,an; 该段代码使用的常量均记作a0。 第i条(1≤i≤k)赋值语句为axi=ayi满足1≤xi≤n、0≤yi≤n,表示将ayi的值赋给变量axi,其中axi被称 为该赋值语句的左值，一定是个变量；ayi被称为右值，可以是一个常量或变量。 对于任意一条赋值语句axi=ayi,如果右值ayi是一个变量，则其应该在此之前被初始化过。 具体来说，如果变量ayi在前i-1条赋值语句中做为左值出现过，即存在j&lt;i满足xj=yi(这里无需考虑第j条赋值语句本身是否也有右值未初始化的问题)，我们就认为在第i条赋值语句中，ayi己被初始化： 否则，我们认为该条语句存在右值未初始化的问题。 按照上述规则，试统计给定的代码中，有多少条赋值语句右值未被初始化。 

【输入格式】 输入的第一行包含空格分隔的两个正整数n、k,分别表示变量的数量和赋值语句的条数。 接下来输入k行，其中第i行(1≤i≤k)包含空格分隔的两个正整数xi,yi表示第i条赋值语句。 

【输出格式】 输出一个整数，表示有右值未被初始化问题的赋值语句条数。

【样例输入】

```
10 7
1 2
3 3
3 0
3 3
6 2
2 1
8 2
```

【样例输出】

```
3
```

## 代码

```cpp
#include &lt;iostream&gt;

using namespace std;

int main()
{
    int N,K,x,y,num=0;
    int a[100000]={0};
    cin&gt;&gt;N&gt;&gt;K;
    for(int i=0;i&lt;K;i++)
    {

        cin&gt;&gt;x&gt;&gt;y;
        if(y==0a[y-1]==1)
            a[x-1]=1;
        else
        {
            a[x-1]=1;
            num++;
        }
    }
    cout &lt;&lt; num &lt;&lt; endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202203-2 出行计划 C++ 100</title><link>https://kaitolab.net/blog/csp-202203-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202203-2/</guid><description>【问题描述】 最近西西艾弗岛上出入各个场所都要持有一定时限内的核酸检测阴性证明。 具体来时，如果在t时刻做了核酸检测，则经过一段时间后可以得到核酸检测阴性证明。这里我们假定等待核酸检测结果需要k个单位时间，即在t+k时刻可以获得结果。...</description><pubDate>Wed, 04 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【问题描述】 最近西西艾弗岛上出入各个场所都要持有一定时限内的核酸检测阴性证明。 具体来时，如果在t时刻做了核酸检测，则经过一段时间后可以得到核酸检测阴性证明。这里我们假定等待核酸检测结果需要k个单位时间，即在t+k时刻可以获得结果。如果一个场所要求持24个单位时间内核酸检测结果入内，那么凭上述的核酸检测结果，可以在第t+k时刻到第t+k+23时刻进入该场所。 小C按时间顺序列出接下来的n项出行计划，其中第i项(1≤i≤n)可以概括为： t时刻进入某场所，该场所需持有ci个单位时间内的核酸检测结果入内，其中0&lt;ci≤2×10^5。 为了合理安排核酸检测时间，试根据小C的出行计划，回答如下查询： ·如果在q时刻做了核酸检测，有多少项出行计划的核酸检测要求可以得到满足？ 这样的查询共有m个，分别为q1，q2,···,qm;查询之间互不影响。 

【输入格式】 输入的第一行包含空格分隔的三个正整数n、m和k,分别表示出行计划数目、查询个数以及等待核酸检测结果所需时间。 接下来输入n行，其中每行包含用空格分隔的两个正整数ti、ci,表示一项出行计划：出行计划按时间顺序给出，满足0&lt;t1≤t2≤···≤tn≤2×10^5. 最后输入 m 行，每行仅包含一个正整数qi，表示一个查询。m个查询亦按照时间顺序给出，满足 0&lt;q1&lt;q2&lt;···&lt;qm≤2×10^5。 

【输出格式】 输出共 m 行，每行一个整数，表示对应查询的答案。 

【样例输入】

```
6 2 10
5 24
10 24
11 24
34 24
35 24
35 48
1
2
```

【样例输出】

```
3
3
```

## 代码

```cpp
#include &lt;iostream&gt;

using namespace std;

const int N = 4e5+5;
int main()
{
    int t[100000],c[100000];
    int b[N]={0};
    int n,m,k,q,left,right;
    cin&gt;&gt;n&gt;&gt;m&gt;&gt;k;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;t[i]&gt;&gt;c[i];
        left=t[i]-c[i]+1;
        left = left&gt;0?left:1;
        right=t[i];
        b[left]++;
        b[right+1]--;
    }

    for(int i=1;i&lt;N;i++)
    {
        b[i]=b[i]+b[i-1];
    }
    while(m--)
    {
        cin&gt;&gt;q;
        cout&lt;&lt;b[q+k]&lt;&lt;endl;
    }
    return 0;
}
```</content:encoded></item><item><title>CSP 202206-1 归一化处理 C++ 100</title><link>https://kaitolab.net/blog/csp-202206-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202206-1/</guid><description>【题目背景】 在机器学习中，对数据进行归一化处理是一种常用的技术。 将数据从各种各样分布调整为平均值为0、方差为1的标准分布，在很多情况下都可以有效地加速模型的训练。</description><pubDate>Fri, 06 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 在机器学习中，对数据进行归一化处理是一种常用的技术。 将数据从各种各样分布调整为平均值为0、方差为1的标准分布，在很多情况下都可以有效地加速模型的训练。 

【问题描述】 这里假定需要处理的数据为n个整数a1,a2,···,an 这组数据的平均值：

$$
\bar{a}\mathrm{=}\frac{a_1+a_2+···+a_n}{n}
$$

方差：

$$
D(a)=\frac{1}{n}\sum_{i=1}^{n}{(a_i-\bar{a})}^2
$$

使用如下函数处理所有数据，得到的n个浮点数f(a1),f(a2),···,f(an)即满足平均值为0且方差为1：

$$
f\left(a_i\right)=\frac{a_i-\bar{a}}{\sqrt{D(a)}}
$$

【输入格式】 从标准输入读入数据。 第一行包含一个整数，表示待处理的整数个数。 第二行包含空格分隔的n个整数，依次表示a1，a2,···,an 

【输出格式】 输出到标准输出。 输出共 n 行，每行一个浮点数，依次表示按上述方法归一化处理后的数据f(a1),f(a2),···,f(an)。 

【样例输入】

```
7
-4 293 0 -22 12 654 1000
```

【样例输出】

```
-0.7485510379073613
0.04504284674812264
-0.7378629047806881
-0.7966476369773906
-0.7057985054006686
1.0096468614303775
1.9341703768876082
```

## 代码

```cpp
#include &lt;bits/stdc++.h&gt;

const int N=1005;

using namespace std;

int main()
{
    int n;
    int a[N];
    double sum=0,avg=0;
    double x,D,sumD;

    cin&gt;&gt;n;
    for (int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;a[i];
        sum+=a[i];
    }
    avg=sum/n;
    for(int i=0;i&lt;n;i++)
    {
        sumD+=pow(a[i]-avg,2);
    }
    D=pow(sumD/n,0.5);
    for(int i=0;i&lt;n;i++)
    {
        x=(a[i]-avg)/D;
        printf(&quot;%f\n&quot;, x);
    }

    return 0;
}
```</content:encoded></item><item><title>CSP 202206-2 寻宝！大冒险！ C++ 100</title><link>https://kaitolab.net/blog/csp-202206-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202206-2/</guid><description>【题目背景】 暑假要到了。可惜由于种种原因，小P原本的出游计划取消。失望的小P只能留在西西艾弗岛上度过一个略显单调的假期…直到… 某天，小P获得了一张神秘的藏宝图。</description><pubDate>Fri, 06 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 暑假要到了。可惜由于种种原因，小P原本的出游计划取消。失望的小P只能留在西西艾弗岛上度过一个略显单调的假期…直到… 某天，小P获得了一张神秘的藏宝图。

【问题描述】 西西艾弗岛上种有棵树，这些树的具体位置记录在一张绿化图上。 简单地说，西西艾弗岛绿化图可以视作一个大小为(L+1)×(L+1)的01矩阵 A, 地图左下角（坐标(0,0）)和右上角（坐标(L,L)分别对应A\[O\]\[O\]和A\[L\]\[L\]。 其中A\[i\]\[j\]=1表示坐标(i,j)处种有一棵树，A\[i\]\[j\]=0则表示坐标(i,j)处没有树。 换言之，矩阵A中有且仅有的 n 个 1 展示了西西艾弗岛上 n 棵树的具体位置。 传说，大冒险家顿顿的宝撥就埋藏在某棵树下。 并且，顿顿还从西西艾弗岛的绿化图上剪下了一小块，制作成藏宝图指示其位置。 具体来说，藏宝图可以看作一个大小为(S+1)×(S+1)的01矩阵B(S远小于L),对应着A中的某一部分。 理论上，绿化图A中存在着一处坐标(x,y)(0≤x,y≤L-S)与潑宝图B左下角(0,0)相对应，即满足： 对B上任意一处坐标(i,j)(0≤i,j≤S),都有A\[x+i\]\[y+j\]=B\[i\]\[j\]。 当上述条件满足时，我们就认为藏宝图 B 对应着绿化图 A 中左下角为(x,y)、右上角为(x+S,y+S)的区域。 实际上，考虑到藏宝图仅描绘了很小的一个范围，满足上述条件的坐标(x,y)很可能存在多个。 请结合西西艾弗岛绿化图中 n 棵树的位置，以及小P手中的藏宝图，判断绿化图中有多少处坐标满足条件。 特别地，藏宝图左下角位置一定是一棵树，即A\[x\]\[y\]=B\[0\]\[0\]=1,表示了宝藏埋藏的位置。

【输入格式】 从标准输入读入数据。 输入的第一行包含空格分隔的三个正整数n、L和S,分别表示西西艾弗岛上树的棵数、绿化图和藏宝图的大小。 由于绿化图尺寸过大，输入数据中仅包含 n 棵树的坐标而非完整的地图；即接下来 n 行每行包含空格分隔的两个整数x和y,表示一棵树的坐标，满足0≤x,y≤L且同一坐标不会重复出现。 最后(S+1)行输入小P手中完整的藏宝图，其中第 i 行(0≤i≤S)包含空格分隔的(S+1)个0和1，表示B\[S-i\]\[0\]···B\[S-i\]\[S\]。 需要注意，最先输入的是B\[S\]\[0\]···B\[S\]\[S\]一行，B\[0\]\[0\]···B\[0\]\[S\]一行最后输入。

【输出格式】 输出到标准输出。 输出一个整数，表示绿化图中有多少处坐标可以与藏宝图左下角对应，即可能埋藏着顿顿的宝藏。

【样例输入】

```
5 100 2
0 0
1 1
2 2
3 3
4 4
0 0 1
0 1 0
1 0 0
```

【样例输出】

```
3
```

## 代码

```cpp
#include &lt;bits/stdc++.h&gt;

using namespace std;

struct node
{
  int x,y;
}tree[1005];

int main()
{
    int n,L,S;
    int B[55][55];
    int x,y,cnt=0;

    memset(B,0,sizeof(B));
    cin&gt;&gt;n&gt;&gt;L&gt;&gt;S;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;x&gt;&gt;y;
        tree[i].x=x;
        tree[i].y=y;
    }
    for(int i=0;i&lt;=S;i++)
    {
        for(int j=0;j&lt;=S;j++)
        {
            cin&gt;&gt;B[S-i][j];
        }
    }
    for(int m=0;m&lt;n;m++)
    {
        int i=0,j=0,flag=1;
        int A[55][55];
        memset(A,0,sizeof(A));
        for(int i=0;i&lt;n;i++)
        {
            if(tree[i].x&gt;=tree[m].x&amp;&amp;tree[i].x&lt;=tree[m].x+S
               &amp;&amp;tree[i].y&gt;=tree[m].y&amp;&amp;tree[i].y&lt;=tree[m].y+S)
                A[tree[i].x-tree[m].x][tree[i].y-tree[m].y]=1;
        }
        for(i=0;flag&amp;&amp;tree[m].x+S&lt;=L&amp;&amp;i&lt;=S;i++)
        {
            for(j=0;flag&amp;&amp;tree[m].y+S&lt;=L&amp;&amp;j&lt;=S;j++)
            {
                if(A[i][j]!=B[i][j])
                    flag=0;
            }
        }
        if(flag==1&amp;&amp;i==S+1&amp;&amp;j==S+1)
            cnt++;
    }
    cout&lt;&lt;cnt&lt;&lt;endl;
    return 0;
}
```</content:encoded></item><item><title>CSP 202206-3 角色授权 C++ 90</title><link>https://kaitolab.net/blog/csp-202206-3/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202206-3/</guid><description>【题目背景】 为了响应国家发展新基建的倡议，西西艾弗岛上兴建了西西艾弗数据中心，并以此为基础运营了西西艾弗云。作为数据中心的运营和维护方， 西西艾弗云公司十分重视西西艾弗云的网络安全管理工作。众所周知，安全性和便捷性难以兼得，同时， ...</description><pubDate>Fri, 06 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目背景】 为了响应国家发展新基建的倡议，西西艾弗岛上兴建了西西艾弗数据中心，并以此为基础运营了西西艾弗云。作为数据中心的运营和维护方， 西西艾弗云公司十分重视西西艾弗云的网络安全管理工作。众所周知，安全性和便捷性难以兼得，同时， 一个混乱的权限模型可能会导致人员被授予不必要的权限，从而造成安全风险。因此在西西艾弗云公司的网络安全部工作的小 C 专门设计了一种科学的权限模型。

这种安全模型将验证流程分为两个步骤。第一步是验证用户的身份（鉴别），第二步是验证用户的权限（授权）。在第一步， 首先验证一个用户是否是该用户所声称的那个身份。例如，通过验证用户提供的口令（Password）是否正确，或者通过验证用户提供的智能卡是否合法有效。 接下来，在授权的步骤中，权限策略会被检索以便判断来访的用户是否能够操作系统中的某个资源。

为了能够灵活地表达用户和授权之间的关系，西西艾弗云公司设计了一种简洁而灵活的授权模型：基于角色的授权模型。它的思路是：首先设定若干角色， 每个角色中指明了一个清单，表明允许访问的资源的种类、资源的名称和对资源的操作；然后将被前一步骤已经鉴别过的用户和一个或多个角色相关联。 某个用户能够执行的操作，即为与其关联的全部角色中允许的操作的并集。

小 C 将实现授权模型的工作交给了你，希望你能够把它们实现出来。

【问题描述】 用户表示授权模型中的一个已识别的主体，该识别过程由此前的鉴别过程完成。一个用户具有下列要素：

· 名称：是一个字符串，用于唯一标识一个用户； · 用户组：是一个数组，包含若干个字符串，表示该用户所属的用户组。 一个待授权的行为，包括下列要素：

· 主体：是一个用户，包括试图进行该行为的用户的名称和该用户所属的用户组； · 操作：是一个字符串，一般是一个动词，例如 Read、Open、Close 等； · 资源：表示该行为的操作对象，由资源种类和资源名称描述。资源种类例如 Door、File 等；在一个特定的资源种类中，资源名称唯一确定了一个资源。 需要注意的是，一个待授权的行为的主体信息，即用户名称和所属用户组，是由前一步骤的鉴别过程完成的。因此，每次授权过程中， 每个待授权的行为都会包含主体用户和其关联的用户组的信息。由于鉴权过程中的其它因素，同一个名称的用户在先后两次待授权的行为中所属的用户组可能有区别， 不能存储或记忆此前每个待授权的行为中，用户与用户组的关联情况，而是要按照每次待授权的行为中给出的信息独立判断。

角色是这种授权模型的基本单位，它指明了一个用户可以执行的操作，角色的清单中描述了角色所允许的操作。一个角色包含下列要素：

· 名称，是一个字符串，用于唯一标识一个角色； · 操作清单，是一个数组，包含一个或多个操作，表示该角色允许执行的操作集合； · 资源种类清单，是一个数组，包含一个或多个资源种类，表示该角色允许操作的资源的种类集合； · 资源名称清单，是一个数组，包含若干个资源名称，表示该角色允许操作的资源的名称集合。 判断一个角色能否对某个资源执行某个操作的过程是：

1.检查该角色的操作清单，如果该角色的操作清单中不包含该操作，且该角色的操作清单中也不包含字符串 _，那么不能执行该操作； 2.检查该角色的资源种类清单，如果该角色的资源种类清单中不包含该资源的种类，且该角色的资源种类清单中也不包含字符串_ ，那么不能执行该操作； 3.检查该角色的资源名称清单，如果该角色的资源名称清单中不包含该资源的名称，且该角色的资源名称清单不是空数组，那么不能执行该操作； 4.允许执行该操作。

例如，假设有某个角色 Doorman，其允许执行的操作有 Open 和 Close，其允许操作的资源类型有 Door，其允许操作的资源名称有 FrontDoor 和 BackDoor。 如果某用户与这个角色关联，那么该用户可以对名为 FrontDoor 的 Door 执行 Open 操作，但是不能对 BackDoor 的 Door 执行 Delete 操作。 同时，一个角色能允许进行的操作可以用通配符来表示。例如，另有一个角色 Admin，其允许执行的操作有 _，允许操作的资源类型是_ ，其允许操作的资源名称列表为空， 那么与该角色关联的所有用户可以执行任何操作。值得注意的是，一个角色的操作清单，只能用允许列表的方式列举该角色允许进行的操作，而不能禁止角色进行某个操作。

角色关联指明了一个用户和一个或多个角色之间的关系。一个角色关联包含下列要素：

· 角色名称，是一个字符串，用于指明一个角色； · 授权对象清单，是一个数组，包含一个或多个用户名称或者用户组名称，表示该角色关联的用户和用户组的集合。 判断一个用户能否执行某个操作的过程是：

1.检查所有的角色关联的授权对象清单，如果清单中包含该用户的名称，或者该清单中包含该用户所属的某一个用户组的名称，那么选取该角色关联所关联的角色； 2.对于所有被选取的角色，判断这些角色是否能对该资源执行该操作，如果所有角色都不能执行该操作，那么不能执行该操作； 3.允许执行该操作。 由此可见，一个角色关联可以将一个角色与多个用户或用户组关联起来。例如，如果有一个角色关联，其关联的角色名称为 Doorman，其关联的用户和用户组清单为 用户 foo1、用户 foo2、用户组 bar。那么这些用户会与 Doorman 角色关联：

· 名为 foo1 的用户，属于用户组 bar； · 名为 foo2 的用户，属于用户组 barz； · 名为 foo3 的用户，属于用户组 bar 和 barz。 但是，属于用户组 barz 的名为 foo4 的用户不能与 Doorman 的角色关联。

从上述判断规则可以知道，一个用户可能与多个角色相关联，在这种情况下，该用户允许进行的操作是这些角色被允许进行的操作集合的并集。

【输入格式】 从标准输入读入数据。

输入的第一行包含三个正整数 n、m、q，分别表示角色数量、角色关联数量和待检查的操作数量。

输入接下来的 n 行中，每行表示一个角色，包括空格分隔的若干元素，依次为：

· 一个字符串，表示该角色的名称； · 一个正整数 nv，表示操作清单中包含的操作数量； · nv 个字符串，依次表示操作清单中的操作； · 一个正整数no ，表示资源种类清单中包含的资源种类的数量； · no 个字符串，依次表示资源种类清单中的资源种类； · 一个非负整数 nn，表示资源名称清单中包含的资源名称的数量； · nn 个字符串，依次表示资源名称清单中的资源名称。 输入接下来的 m 行中，每行表示一个角色关联，包括空格分隔的若干元素，依次为：

· 一个字符串，表示该角色关联的角色名称； · 一个正整数 ns，表示授权对象清单中包含的授权对象的数量； · 2ns 个字符串，每两个表示授权对象清单中的授权对象，前一个字符串为 u 或 g，分别表示这个授权对象是一个用户名称或者用户组名称，后一个字符串为用户名称或者用户组名称。 输入接下来的 q 行中，每行表示一个待授权的行为，包括空格分隔的若干元素，依次为：

· 一个字符串，表示执行该操作的用户名称； · 一个正整数 ng，表示该用户所属的用户组的数量； · ng 个字符串，依次表示该用户所属的用户组的名称； · 一个字符串，表示待查操作的名称； · 一个字符串，表示被操作的资源种类； · 一个字符串，表示被操作的资源名称。 

【输出格式】 输出到标准输出。

输出 q 行，每行表示一个操作是否可以被执行，0 表示不能执行，1 表示可以执行。

【样例输入】

```
1 2 3
op 1 open 1 door 0
op 1 g sre
op 1 u xiaop
xiaoc 2 sre ops open door room302
xiaop 1 ops open door room501
xiaoc 2 sre ops remove door room302
```

【样例输出】

```
1
1
0
```

【样例解释】 在本例中，定义了一个名为 op 的角色，授予了对任意 door 类型的对象的 open 操作的权限，同时定义了两个指向 op 的角色关联。 注意，可以针对一个角色定义多于一个角色关联。本例给出了三个待授权的行为。其中，第一个行为，授权的主体用户是 xiaoc， 该用户所属的用户组 sre 被关联 op 角色，因此可以执行开门动作。第二个行为中，授权的主体用户是 xiaop， 该用户被直接关联了 op 角色，因此也可以执行开门动作。第三个行为中，授权的主体用户仍是 xiaoc，关联的角色仍为 op。但是， 由于 op 角色并未被授予 remove 操作的权限，因此该动作被拒绝。

## 代码

```cpp
#include &lt;bits/stdc++.h&gt;

using namespace std;

struct s_role
{
    string rolename;
    int nv=0;
    string opts[400];
    int no=0;
    string s_kind[400];
    int nn=0;
    string s_name[400];
}role[500];

int main()
{
    int n,m,q;
    int nv,no,nn,ns,ng;
    map&lt;string,int&gt; dict;
    map&lt;string,vector&lt;int&gt;&gt; user_role;//用户对应角色
    map&lt;string,vector&lt;int&gt;&gt; group_role;//用户组对应角色
    cin&gt;&gt;n&gt;&gt;m&gt;&gt;q;
    for(int i=0;i&lt;n;i++)
    {
        cin&gt;&gt;role[i].rolename;
        dict[role[i].rolename]=i;
        cin&gt;&gt;nv;
        role[i].nv=nv;
        for(int j=0;j&lt;nv;j++)
        {
            cin&gt;&gt;role[i].opts[j];
        }
        cin&gt;&gt;no;
        role[i].no=no;
        for(int j=0;j&lt;no;j++)
        {
            cin&gt;&gt;role[i].s_kind[j];
        }
        cin&gt;&gt;nn;
        role[i].nn=nn;
        for(int j=0;j&lt;nn;j++)
        {
            cin&gt;&gt;role[i].s_name[j];
        }
    }
    for(int i=0;i&lt;m;i++)
    {
        string rolename;
        cin&gt;&gt;rolename;
        cin&gt;&gt;ns;
        for(int j=0;j&lt;ns;j++)
        {
            string username,group;
            char type;
            cin&gt;&gt;type;
            if(type==&apos;u&apos;)
            {
                cin&gt;&gt;username;
                user_role[username].push_back(dict[rolename]);
            }
            else if(type==&apos;g&apos;)
            {
                cin&gt;&gt;group;
                group_role[group].push_back(dict[rolename]);
            }
        }
    }

    string username,w_opt,w_kind,w_name;
    string group[400];
    for(int i=0;i&lt;q;i++)
    {
        cin&gt;&gt;username;
        cin&gt;&gt;ng;
        for(int j=0;j&lt;ng;j++)
        {
            cin&gt;&gt;group[j];
        }
        cin&gt;&gt;w_opt;
        cin&gt;&gt;w_kind;
        cin&gt;&gt;w_name;

        int action=0;
        int role_index;

        //查找该用户对应的角色可以执行的操作
        for(unsigned i2=0;action==0&amp;&amp;user_role.count(username)&gt;0&amp;&amp;i2!=user_role[username].size();i2++)
        {
            role_index=user_role[username][i2];
            for(int j=0;action==0&amp;&amp;j&lt;role[role_index].nv;j++)
            {
                //角色操作符合要求
                if(role[role_index].opts[j]==&quot;*&quot;role[role_index].opts[j]==w_opt)
                {
                    for(int l=0;action==0&amp;&amp;l&lt;role[role_index].no;l++)
                    {
                        //资源名称符合要求
                        if(role[role_index].s_kind[l]==&quot;*&quot;role[role_index].s_kind[l]==w_kind)
                        {
                            if(role[role_index].nn==0)
                                action=1;
                            for(int k=0;action==0&amp;&amp;k&lt;role[role_index].nn;k++)
                            {
                                if(role[role_index].s_name[k]==w_name)
                                {
                                    action=1;
                                }
                            }
                        }
                    }
                }
            }
        }
        for(int i1=0;i1&lt;ng;i1++)
        {
            string groupname;
            groupname=group[i1];
            //查找该用户所在的用户组对应的角色可以执行的操作
            for(unsigned i2=0;action==0&amp;&amp;group_role.count(groupname)&gt;0&amp;&amp;i2!=group_role[groupname].size();i2++)
            {
                role_index=group_role[groupname][i2];
                for(int j=0;action==0&amp;&amp;j&lt;role[role_index].nv;j++)
                {
                    //角色操作符合要求
                    if(role[role_index].opts[j]==&quot;*&quot;role[role_index].opts[j]==w_opt)
                    {
                        for(int l=0;action==0&amp;&amp;l&lt;role[role_index].no;l++)
                        {
                            //资源类型符合要求
                            if(role[role_index].s_kind[l]==&quot;*&quot;role[role_index].s_kind[l]==w_kind)
                            {
                                if(role[role_index].nn==0)
                                    action=1;
                                for(int k=0;action==0&amp;&amp;k&lt;role[role_index].nn;k++)
                                {
                                    if(role[role_index].s_name[k]==w_name)
                                    {
                                        action=1;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if(action==1)
            cout&lt;&lt;1&lt;&lt;endl;
        else
            cout&lt;&lt;0&lt;&lt;endl;
    }

    return 0;
}
```</content:encoded></item><item><title>CSP 202209-1 如此编码 C++ 100</title><link>https://kaitolab.net/blog/csp-202209-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202209-1/</guid><description>【题目描述】 己知某次测验包含n道单项选择题，其中第i题(1≤i≤n)有ai个选项，正确选项为bi,满足ai≥2且 0≤bi&lt;ai。比如说，ai=4表示第i题有4个选项，此时正确选项bi,的取值一定是0、1、2、3其中之一。 顿顿老师...</description><pubDate>Fri, 06 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 己知某次测验包含n道单项选择题，其中第i题(1≤i≤n)有ai个选项，正确选项为bi,满足ai≥2且 0≤bi&lt;ai。比如说，ai=4表示第i题有4个选项，此时正确选项bi,的取值一定是0、1、2、3其中之一。 顿顿老师设计了如下方式对正确答案进行编码，使得仅用一个整数m便可表示b1，b2,···,bn 首先定义一个辅助数组ci,表示数组ai的前缀乘积。当1≤i≤n时，满足：

$C_i=a_1×a_2×···×a_i$

特别地，定义c0=1。 于是 m 便可按照如下公式算出：

$m=\sum_{i=1}^{n}c_i×b_i\\
=c_0b_1+c_1b_2+···+c_{n-1}b_n$
易知，0≤m&lt;cn,最小值和最大值分别当bi全部为0和bi=ai-1时取得。 试帮助小P同学，把测验的正确答案b1,b2,···,bn从顿顿老师留下的神秘整数m中恢复出来。 

【输入格式】 从标准输入读入数据。 输入共两行。 第一行包含用空格分隔的两个整数n和m,分别表示题目数量和顿顿老师的神秘数字。 第二行包含用空格分隔的n个整数a1,a2,···,an,依次表示每道选择题的选项数目。 

【输出格式】 输出到标准输出。 输出仅一行，包含用空格分隔的n个整数b1,b2,···,bn,依次表示每道选择题的正确选项。 【样例输入】

```
15 32767
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
```

【样例输出】

```
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
```

## 代码

```cpp
#include &lt;bits/stdc++.h&gt;

using namespace std;
using ll=long long;
const ll N=100;

int main()
{
    ll n,m;
    ll c[N];
    ll a,t;
    cin&gt;&gt;n&gt;&gt;m;
    c[0]=1;
    for(ll i=1;i&lt;=n;i++)
    {
        cin&gt;&gt;a;
        c[i]=c[i-1]*a;
    }

    for(ll i=1;i&lt;=n;i++)
    {
        t=m%c[i]-m%c[i-1];
        cout&lt;&lt;t/c[i-1]&lt;&lt;&apos; &apos;;
    }

    return 0;
}
```</content:encoded></item><item><title>CSP 202209-2 何以包邮？ C++ 70</title><link>https://kaitolab.net/blog/csp-202209-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/csp-202209-2/</guid><description>【题目描述】 新学期伊始，适逄顿顿书城有购书满x元包邮的活动，小P同学欣然前往准备买些参考书。 一番浏览后，小P初步筛选出 n 本书加入购物车中，其中第 i 本（1≤i≤n)的价格为 ai 元。 考虑到预算有限，在最终付款前小P决定再...</description><pubDate>Fri, 06 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

【题目描述】 新学期伊始，适逄顿顿书城有购书满x元包邮的活动，小P同学欣然前往准备买些参考书。 一番浏览后，小P初步筛选出 n 本书加入购物车中，其中第 i 本（1≤i≤n)的价格为 ai 元。 考虑到预算有限，在最终付款前小P决定再从购物车中删去几本书（也可以不删），使得剩余图书的价格总和m在满足 包邮条件(m≥x)的前提下最小。 试帮助小P计算，最终选购哪些书可以在凑够x元包邮的前提下花费最小？ 

【输入格式】 从标准输入读入数据。 输入的第一行包含空格分隔的两个正整数 n 和 x ,分别表示购物车中图书数量和包邮条件。 接下来输入 n 行，其中第 i 行(1≤i≤n)仅包含一个正整数 ai ,表示购物车中第 i 本书的价格。输入数据保证 n 本 书的价格总和不小于 x 。 

【输出格式】 输出到标准输出。 仅输出一个正整数，表示在满足包邮条件下的最小花费。 

【样例输入】

```
4 100
20
90
60
60
```

【样例输出】

```
110
```

## 代码

```cpp
#include &lt;bits/stdc++.h&gt;

using namespace std;
using ll=long long;
const ll N=35;
static ll n,x,minsum=12e5;

bool cmp(ll a,ll b)
{
    return b&lt;a;
}

int buy(ll i,ll sum,ll a[N])
{
    if(i+1&lt;n)
    {
        buy(i+1,sum,a);
        sum+=a[i];
        if(sum&lt;minsum&amp;&amp;sum&gt;=x)
            minsum=sum;
        buy(i+1,sum,a);
    }
    else
    {
        sum+=a[i];
        if(sum&lt;minsum&amp;&amp;sum&gt;=x)
            minsum=sum;
    }
    return 0;
}

int main()
{
    ll a[N];//price
    ll sum=0;
    cin&gt;&gt;n&gt;&gt;x;

    for(ll i=0;i&lt;n;i++)
    {
        cin&gt;&gt;a[i];
    }
    sort(a,a+n,cmp);
    buy(0,sum,a);
    cout&lt;&lt;minsum&lt;&lt;endl;

    return 0;
}
```</content:encoded></item><item><title>个人博客部署到阿里云服务器全攻略</title><link>https://kaitolab.net/blog/deploy-to-aliyun/</link><guid isPermaLink="true">https://kaitolab.net/blog/deploy-to-aliyun/</guid><description>从 GitHub Pages 迁移到阿里云 ECS，完整记录 Nginx 配置、SELinux 排障、CI/CD 自动部署的全过程与踩坑经验。</description><pubDate>Sat, 06 Jun 2026 16:00:00 GMT</pubDate><content:encoded>## 缘起

本站一开始部署在 GitHub Pages 上，免费省心，但有两个问题始终让我不太舒服：

1. **访问速度不稳定**，国内加载有时候慢得离谱
2. **URL 带子路径** `/astro-kaito/`，不够清爽

于是一咬牙买了台阿里云 ECS，打算把博客迁移过去。本以为一两个小时搞定，结果踩了一堆坑。这篇文章把整个过程记录下来，给后来人趟个路。

## 服务器选型

阿里云 ECS 新人白嫖价一年 68 块，配置对我这种纯静态博客来说绰绰有余：

| 项目 | 配置 |
|------|------|
| 实例规格 | 2 vCPU / 2 GiB 内存 |
| 操作系统 | Alibaba Cloud Linux 3 |
| 系统盘 | 40 GiB ESSD |
| 带宽 | 按流量计费，5 Mbps 峰值 |

&gt; **为什么选 Alibaba Cloud Linux 3 而不是 Ubuntu？** CentOS 停更后，这是阿里云主推的免费 Linux，对自家 ECS 兼容性最好。

## 第一步：环境搭建

SSH 登录后装 Nginx 和 Node.js。

### 安装 Nginx

```bash
sudo dnf install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
```

打开浏览器访问 `http://公网IP`，看到 Nginx 欢迎页说明 OK。

&gt; **注意**：Alibaba Cloud Linux 3 用 `dnf` 而不是 `apt`。一开始我习惯性敲 `apt` 报错 command not found，愣了三秒才反应过来。

### 安装 Node.js 22

Astro 需要 Node.js ≥ 22。直接装官方源：

```bash
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install nodejs -y
node -v  # 确认版本
```

### 克隆代码并构建

```bash
sudo mkdir -p /var/www/kaitohub
sudo git clone https://github.com/korolkk/astro-kaito.git /var/www/kaitohub
cd /var/www/kaitohub
npm install
npm run build
```

构建完成后 `dist/` 目录就是我们需要部署的纯静态文件。

## 第二步：Nginx 配置

Alibaba Cloud Linux 3 的 Nginx 配置结构和 Ubuntu 不太一样——没有 `sites-available/` 和 `sites-enabled/` 目录，直接就一个 `/etc/nginx/nginx.conf`。

```nginx
user nginx;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile      on;
    tcp_nopush    on;

    # Gzip 压缩
    gzip            on;
    gzip_types      text/plain text/css application/json
                    application/javascript text/xml image/svg+xml;
    gzip_min_length 256;
    gzip_vary       on;

    server {
        listen       80;
        server_name  你的域名.com;

        root   /var/www/kaitohub/dist;
        index  index.html;

        # 静态资源长期缓存（文件名带 hash）
        location /_astro/ {
            expires 1y;
            add_header Cache-Control &quot;public, immutable&quot;;
        }

        # SPA 风格路由
        location / {
            try_files $uri $uri/ $uri.html /404.html =404;
        }

        error_page 404 /404.html;
    }
}
```

配置检查并重载：

```bash
sudo nginx -t          # 检查语法
sudo systemctl reload nginx
```

## 第三步：SSL 证书

Let&apos;s Encrypt 免费证书 + certbot 自动续期：

```bash
sudo dnf install certbot python3-certbot-nginx -y
sudo certbot --nginx -d 你的域名.com
sudo certbot renew --dry-run   # 测试自动续期
```

## 第四步：踩坑记录

前面说的都是顺利的，真正折磨人的是下面这些坑。

### 坑一：页面只有文字，CSS 和图片全部 404

部署完打开网站，白底黑字，像个 1998 年的网页。

**原因**：本站之前部署在 GitHub Pages 上，URL 子路径是 `/astro-kaito/`。`astro.config.mjs` 里写死了 `base: &apos;/astro-kaito&apos;`，所有 CSS/JS/图片的引用路径都带了 `/astro-kaito/` 前缀。Nginx 从根路径 `/` 提供文件，`/astro-kaito/_astro/style.css` 这个路径根本不存在。

**修复**：把 `base` 改成环境变量控制：

```js
// astro.config.mjs
base: process.env.BASE_URL || &apos;/&apos;,
```

- 阿里云构建时：`BASE_URL` 未设置，默认 `/`
- GitHub Pages 构建时：GitHub Actions 注入 `BASE_URL: /astro-kaito`

两边都能正确部署。

### 坑二：SELinux 拦截 Nginx 读取文件

Alibaba Cloud Linux 3 默认开启 SELinux（Enforcing 模式）。Nginx 配置明明正确，文件权限也对，但就是返回 403 Forbidden 或 404。

**排查方法**：

```bash
# 查看 SELinux 审计日志
sudo ausearch -m avc -ts recent

# 临时关闭 SELinux 验证是否是它的问题
sudo setenforce 0
```

果然是 SELinux。Nginx 进程的上下文（`httpd_t`）没有权限读取 `/var/www/kaitohub/dist/` 目录下的文件。

**永久修复**（不要关闭 SELinux，而是正确配置）：

```bash
# 安装 SELinux 管理工具
sudo dnf install policycoreutils-python-utils -y

# 设置目录默认 SELinux 上下文（永久生效）
sudo semanage fcontext -a -t httpd_sys_content_t &quot;/var/www/kaitohub/dist(/.*)?&quot;

# 立即应用
sudo restorecon -R /var/www/kaitohub/dist/

# 允许 Nginx 发起网络连接
sudo setsebool -P httpd_can_network_connect on
```

&gt; **教训**：在 Alibaba Cloud Linux 3 / CentOS / RHEL 系系统上部署 Web 服务，SELinux 永远是头号嫌疑犯。文件权限设对了还不够，SELinux 上下文也得对。而且 `semanage` 配好了就一劳永逸，以后每次部署只需跑 `restorecon` 即可。

### 坑三：GitHub Actions 自动部署的五个连环坑

手动 `scp` 上传几次后实在受不了，决定上 CI/CD。结果光是搭 GitHub Actions 自动部署就踩了五个连环坑。

#### 第一步：配置 Secrets

GitHub Actions 需要通过 SSH 连接到阿里云服务器。需要配置两个东西：

1. **SSH 密钥对**：让 GitHub Actions 免密登录服务器
2. **GitHub Secrets**：安全存储密钥和服务器 IP

**本地生成密钥**：

```powershell
# Windows PowerShell 中执行
ssh-keygen -t ed25519 -f &quot;$env:USERPROFILE\.ssh\aliyun-deploy&quot;
```

提示输入密码时直接回车跳过（CI 环境无法交互式输入密码）。

**上传公钥到服务器**：

```powershell
# Windows 没有 ssh-copy-id，用管道代替
type &quot;$env:USERPROFILE\.ssh\aliyun-deploy.pub&quot; | ssh root@你的服务器IP &quot;mkdir -p ~/.ssh &amp;&amp; cat &gt;&gt; ~/.ssh/authorized_keys &amp;&amp; chmod 600 ~/.ssh/authorized_keys&quot;
```

**配置 GitHub Secrets**：打开仓库 Settings → Secrets and variables → Actions，添加两个 Repository secrets：

| Name | Value |
|------|-------|
| `ALIYUN_HOST` | ECS 公网 IP |
| `ALIYUN_SSH_KEY` | `type ~\.ssh\aliyun-deploy` 输出的完整私钥内容 |

#### 连环坑一：第三方 Action 版本不存在

一开始用了 `easingthemes/ssh-deploy@v5`，结果 CI 直接报错：

```
Error: Unable to resolve action `easingthemes/ssh-deploy@v5`, unable to find version `v5`
```

后来查了一下，这个 Action 根本没有 v5 标签。第三方 Action 的版本号说变就变，线上排错极其痛苦。

**修复**：删掉第三方 Action，直接用 GitHub Actions runner 自带的 SSH 和 tar 命令完成部署。零外部依赖，不需要猜版本号。

#### 连环坑二：secrets 不能用于 job 级别 if 条件

为了让没配 Secrets 的仓库也能正常跑 CI（只部署 GitHub Pages、自动跳过阿里云），我写了这样一个条件：

```yaml
jobs:
  deploy-aliyun:
    if: ${{ secrets.ALIYUN_HOST != &apos;&apos; }}   # ❌ 报错！
```

结果 GitHub Actions 解析器直接报错：

```
Unrecognized named-value: &apos;secrets&apos;. Located at position 1 within expression: secrets.ALIYUN_HOST != &apos;&apos;
```

**原因**：GitHub Actions 不允许在 job 级别的 `if` 表达式中直接访问 `secrets` 上下文，出于安全考虑。

**修复**：先在 step 中通过 `env` 桥接 secret 值，然后用 step output 控制后续步骤：

```yaml
steps:
  - name: Check if Aliyun secrets configured
    id: check
    env:
      HOST: ${{ secrets.ALIYUN_HOST }}
    run: |
      if [ -n &quot;$HOST&quot; ]; then
        echo &quot;configured=true&quot; &gt;&gt; $GITHUB_OUTPUT
      else
        echo &quot;configured=false&quot; &gt;&gt; $GITHUB_OUTPUT
      fi

  - name: Checkout
    if: steps.check.outputs.configured == &apos;true&apos;
    uses: actions/checkout@v4

  # ... 后续步骤全部加上这个 if 条件
```

#### 连环坑三：服务端缺少 rsync

最初用 rsync 传输文件：

```bash
rsync -avz --delete -e &quot;ssh -i key&quot; dist/ root@$HOST:/var/www/kaitohub/dist/
```

报错：

```
bash: rsync: command not found
rsync: connection unexpectedly closed
```

**原因**：rsync 协议需要**两端**都安装 rsync——你的电脑（CI runner）是发送端，服务器是接收端。Alibaba Cloud Linux 3 默认没装 rsync。

**修复**：改用 tar 管道传输。tar 是 Linux 标配，不需要额外安装任何东西：

```bash
tar -czf - -C dist . | \
  ssh -i key root@$HOST \
    &quot;rm -rf /var/www/kaitohub/dist/* &amp;&amp; \
     tar -xzf - -C /var/www/kaitohub/dist/ &amp;&amp; \
     restorecon -R /var/www/kaitohub/dist/ &amp;&amp; \
     systemctl reload nginx&quot;
```

一句话完成：打包 → 上传 → 解压 → SELinux 修复 → 重载 Nginx。完全不依赖服务端任何非系统自带工具。

#### 连环坑四：tar 解压后 SELinux 上下文丢失

tar 管道解决了传输问题，但 CI 又报了新错误：

```
chcon: can&apos;t apply partial context to unlabeled file &apos;favicon.ico&apos;
```

**原因**：GitHub Actions 的 Ubuntu runner 没有 SELinux（内核就根本不支持），从那里打包的 tar.gz 文件内部不带任何 SELinux 标签。解压到开启了 SELinux 的服务器后，这些文件处于&quot;未标记&quot;（unlabeled）状态，`chcon` 无法对其设置上下文。

**修复**：两件事配合——

1. **服务器端一次性配置**（坑二中已做）：用 `semanage` 设置目录默认上下文
2. **CI 中用 `restorecon` 替代 `chcon`**：`restorecon` 根据策略文件恢复默认上下文，对无标签文件也能正常工作

```bash
# CI 中的命令最终版
tar -czf - -C dist . | \
  ssh -i key root@$HOST \
    &quot;rm -rf /var/www/kaitohub/dist/* &amp;&amp; \
     tar -xzf - -C /var/www/kaitohub/dist/ &amp;&amp; \
     restorecon -R /var/www/kaitohub/dist/ &amp;&amp; \
     systemctl reload nginx&quot;
```

#### 连环坑五：一个仓库两套 base 路径

GitHub Pages 需要 `base: &apos;/astro-kaito&apos;`，阿里云需要 `base: &apos;/&apos;`。同一个仓库要同时部署到两个地方，构建参数不一样。

**解决方案**：workflow 里拆成两个独立的 job，各自用不同的 `BASE_URL` 构建：

```yaml
# 阿里云构建
- name: Build
  run: npm run build
  env:
    BASE_URL: /

# GitHub Pages 构建
- name: Build
  run: npm run build
  env:
    BASE_URL: /astro-kaito
```

两个 job 并行执行，互不干扰，各自产出正确路径的 dist 文件。

### 完整 workflow 最终版

经过上述五个连环坑的打磨，最终的 deploy.yml 如下（阿里云部署部分）：

```yaml
jobs:
  deploy-aliyun:
    runs-on: ubuntu-latest
    steps:
      - name: Check if Aliyun secrets configured
        id: check
        env:
          HOST: ${{ secrets.ALIYUN_HOST }}
        run: |
          if [ -n &quot;$HOST&quot; ]; then
            echo &quot;configured=true&quot; &gt;&gt; $GITHUB_OUTPUT
          else
            echo &quot;configured=false&quot; &gt;&gt; $GITHUB_OUTPUT
          fi

      - name: Checkout
        if: steps.check.outputs.configured == &apos;true&apos;
        uses: actions/checkout@v4

      - name: Setup Node
        if: steps.check.outputs.configured == &apos;true&apos;
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        if: steps.check.outputs.configured == &apos;true&apos;
        run: npm ci

      - name: Build
        if: steps.check.outputs.configured == &apos;true&apos;
        run: npm run build
        env:
          BASE_URL: /

      - name: Deploy to Alibaba Cloud
        if: steps.check.outputs.configured == &apos;true&apos;
        env:
          HOST: ${{ secrets.ALIYUN_HOST }}
          SSH_KEY: ${{ secrets.ALIYUN_SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo &quot;$SSH_KEY&quot; &gt; ~/.ssh/aliyun_deploy_key
          chmod 600 ~/.ssh/aliyun_deploy_key
          ssh-keyscan -H $HOST &gt;&gt; ~/.ssh/known_hosts
          tar -czf - -C dist . | \
            ssh -i ~/.ssh/aliyun_deploy_key root@$HOST \
              &quot;rm -rf /var/www/kaitohub/dist/* &amp;&amp; \
               tar -xzf - -C /var/www/kaitohub/dist/ &amp;&amp; \
               restorecon -R /var/www/kaitohub/dist/ &amp;&amp; \
               systemctl reload nginx&quot;
```

## 最终效果

现在 `git push` 之后，GitHub Actions 自动完成：

1. 用 `base: /` 构建 → tar 推送 + 解压到阿里云 → `restorecon` 修 SELinux → 重载 Nginx
2. 用 `base: /astro-kaito` 构建 → 上传到 GitHub Pages Artifact → 部署到 Pages

整个过程不到 2 分钟，完全自动化。没配 Secrets 的话自动跳过阿里云部署，不影响 GitHub Pages。

## 总结

部署静态博客到阿里云其实不复杂，核心就三件事：

1. **Nginx 指向 dist 目录**
2. **SELinux 文件上下文正确**（`semanage` 一劳永逸，`restorecon` 日常修复）
3. **Astro base 路径匹配实际部署路径**（环境变量控制，多目标构建）

真正花时间的是那些&quot;明明配置都对但就是不工作&quot;的时刻。主要坑都在 CI/CD 环节：

| 坑 | 症状 | 教训 |
|---|------|------|
| 第三方 Action | 版本不存在报错 | 能用原生命令就别用第三方 Action |
| secrets 在 if | workflow 语法报错 | GitHub Actions 有严格的安全限制 |
| rsync 缺失 | 传输失败 | tar 管道是万能替代方案 |
| SELinux 无标签 | chcon 报错 | `semanage` + `restorecon` 才是正确姿势 |

好在我们有 AI——这篇文章中大部分排障过程都是 Claude 一步一步辅助完成的。

如果你也在折腾博客部署，希望这篇踩坑记录能让你少走弯路。</content:encoded></item><item><title>深度图像转换为点云数据计算原理及代码实现</title><link>https://kaitolab.net/blog/depth-image-to-point-cloud/</link><guid isPermaLink="true">https://kaitolab.net/blog/depth-image-to-point-cloud/</guid><description>首先，要了解下世界坐标到图像的映射过程，考虑世界坐标点M(Xw,Yw,Zw)映射到图像点m(u,v)的过程，如下图所示：</description><pubDate>Mon, 22 May 2023 16:00:00 GMT</pubDate><content:encoded>## 原理

首先，要了解下世界坐标到图像的映射过程，考虑世界坐标点M(Xw,Yw,Zw)映射到图像点m(u,v)的过程，如下图所示：

![img](深度图像转换为点云数据计算原理及代码实现/545378-20220318161349260-2010534016.png)

**形式化表示如下**：

![img](深度图像转换为点云数据计算原理及代码实现/545378-20220318161414719-892949689.png)

u,v分别为图像坐标系下的任意坐标点。u0,v0分别为图像的中心坐标。xw,yw,zw表示世界坐标系下的三维坐标点。zc表示相机坐标的z轴值，即目标到相机的距离。R,T分别为外参矩阵的3x3旋转矩阵和3x1平移矩阵。

**对外参矩阵的设置：由于世界坐标原点和相机原点是重合的，即没有旋转和平移，所以**

![img](深度图像转换为点云数据计算原理及代码实现/545378-20220318161453684-97553231.png)

相机坐标系和世界坐标系的坐标原点重合，因此相机坐标和世界坐标下的同一个物体具有相同的深度，即zc=zw.于是公式可进一步简化为：

![img](深度图像转换为点云数据计算原理及代码实现/545378-20220318161547547-572347478.png)

从以上的变换矩阵公式，可以计算得到图像点\[u,v\]T 到世界坐标点\[xw,yw,zw\]T的变换公式:

![img](深度图像转换为点云数据计算原理及代码实现/545378-20220318161616407-1391108753.png)

这里Zc是深度值，相机内参为：fx=f/dx, fy= f/dy, cx=u0, cy = v0;

![](深度图像转换为点云数据计算原理及代码实现/v2-6296a73c317812a0d8654f5480d64095_1440w-300x110.png)

## 代码实现

```c++
void OBCameraNode::Get_World_Pix_By_Depth(float k[],float scale,int color_x, int color_y, short dep_z, float&amp; world_x, float&amp; world_y, float&amp; world_z) {

    float u=(float)color_x;
    float v=(float)color_y;
    float depthv =dep_z*scale;
    // k[0]:fx k[1]:cx 
    // k[2]:fy k[3]:cy
    world_z = depthv;
    world_x = (u - k[1]) * depthv / k[0];
    world_y = (v - k[3]) * depthv / k[2];
}
```</content:encoded></item><item><title>博客接入 AI 助手：OpenClaw 桥接方案从零搭建</title><link>https://kaitolab.net/blog/integrate-openclaw-ai-chat/</link><guid isPermaLink="true">https://kaitolab.net/blog/integrate-openclaw-ai-chat/</guid><description>从架构设计到前端实现，完整记录在 Astro 静态博客中集成 OpenClaw AI 聊天和文章问答功能的全过程。</description><pubDate>Mon, 08 Jun 2026 16:00:00 GMT</pubDate><content:encoded>## 缘起

博客搭好之后总觉得少了点什么。GitHub 热力图、数字滚动动画、打字机效果都加了，但都是&quot;静态&quot;的趣味功能。我想让博客真的能&quot;对话&quot;——访客可以问我博客里的内容，或者随便聊聊天。

正好我在阿里云 ECS 上跑着 OpenClaw 网关，它提供了标准的 OpenAI 兼容 API。于是花了半天时间，把 AI 聊天和文章问答功能接入了博客前台。

## 架构设计

最直接的方案是让前端直接调 OpenClaw 的 API。但这有几个问题：

1. **安全**：OpenClaw 的管理 token 会暴露在浏览器里
2. **不可控**：没法做限流、没法定制 system prompt
3. **耦合**：前端要知道 OpenClaw 的地址和认证方式

所以加了一层 **API Bridge**（Node.js Express 服务，~250 行）：

```
浏览器 → Nginx → API Bridge (:3001) → OpenClaw (:18789)
```

Bridge 负责三件事：注入 AI 人格、限流保护、格式化响应。前端只和 Bridge 通信，完全不感知后端的 OpenClaw。

## API Bridge 四个端点

| 端点 | 方法 | 功能 |
|------|------|------|
| `/api/chat` | POST | 自由聊天，注入 system prompt 定义 AI 人格 |
| `/api/article-qa` | POST | 文章问答，自动注入文章全文做上下文 |
| `/api/deploy` | POST | Webhook 部署，CI 推送后服务器自动拉取构建 |
| `/api/health` | GET | 健康检查 |

其中 `/api/deploy` 是后来加的——既然 Bridge 已经在跑了，顺便用它接收 GitHub Actions 的部署通知，省掉了 SSH 那一整套东西。

### System Prompt 设计

定义 AI 分身的人格是关键。我给它设了三个维度的约束：

- **身份**：嵌入式软件工程师，专注音视频开发，主攻 C/C++
- **风格**：专业但不枯燥，能用通俗类比解释技术概念，中文为主
- **边界**：只回答技术相关问题，拒绝政治/违法内容，不透露内部配置

这样 AI 的回答风格和博客主题一致，不会出现&quot;我是一个通用 AI 助手&quot;那种出戏的感觉。

### 限流策略

AI 调用是有成本的（即使是本地跑），不加限制容易被滥用。三级限流，全部通过环境变量可调：

| 端点 | 默认限制 | 原因 |
|------|----------|------|
| `/api/chat` | 20 次/分钟/IP | 自由聊天调用频率高 |
| `/api/article-qa` | 10 次/分钟/IP | 附带长文本，最贵 |
| 全局兜底 | 60 次/分钟/IP | 防止异常流量 |

用 `express-rate-limit` 实现，超限返回 HTTP 429，前端会显示 &quot;请求太频繁了，请稍后再试 🌊&quot;。

## 前端组件

遵循 Astro 的&quot;零 JS 默认&quot;理念，全部用内联 `&lt;script is:inline&gt;` + IIFE 实现，不引入任何框架，不增加外部 JS 文件。

### 浮窗 AI 聊天

右下角 48px 紫蓝渐变圆形按钮，比返回顶部按钮更靠上、更显眼。外层有脉冲光环动画，hover 时放大 + 阴影加深。

点击弹出 380×500px 对话框，包含：
- **头部**：💬 Kaito AI · 在线 · 随时可聊
- **消息区**：用户消息靠右紫蓝渐变气泡，AI 回复靠左灰色气泡。不加&quot;你&quot;/&quot;AI&quot;之类的机械标签，靠颜色和位置自然区分，和开场问候 &quot;👋 你好！我是 Kaito 的 AI 分身&quot; 的语气保持一致
- **输入区**：支持 Enter 发送 / Shift+Enter 换行，移动端自动适配
- **打字指示器**：三个跳动圆点，等 AI 回复时不会焦虑

暗色模式自动跟随博客主题，所有颜色走 CSS 变量。

### 文章 AI 问答

每篇博客底部、评论区上方插入一个问答区块：
- 3 个预设问题快捷按钮（核心观点、优缺点、适合人群）
- 自定义输入框
- 回答区带加载动画

点击预设按钮会自动把问题 + 文章标题 + 文章内容（前 8000 字）发给 `/api/article-qa`。Bridge 收到后构造包含完整上下文的 prompt 再请求 OpenClaw，确保回答基于文章内容而不是瞎编。

## 部署

### 服务端

API Bridge 是独立的 Node.js 服务，部署在 `/opt/api-bridge/`，通过 systemd 管理：

```ini
[Service]
ExecStart=/usr/bin/node /opt/api-bridge/server.js
Restart=on-failure
```

依赖 `express` + `cors` + `dotenv` + `express-rate-limit`，轻量到几乎没有开销。

### Nginx 代理

```nginx
location /api/ {
    proxy_pass http://127.0.0.1:3001;
}
```

前端用相对路径 `/api/chat` 发请求，无论 IP 还是域名都能正常工作。

## 总结

整个方案的精髓在于**加一层桥接**。不直接暴露 OpenClaw，换来的是：

- **安全**：token 不泄露到浏览器
- **可控**：限流、定制 prompt 全部在服务端
- **解耦**：前后端各自独立演进，Bridge 还可以顺便干别的事（比如 webhook 部署）
- **零前端框架**：纯内联 JS，不增加页面体积，Lighthouse 评分不受影响

如果你的博客也有本地跑的 AI 服务，这套架构基本可以直接复用——换掉 system prompt 和 OpenClaw 的 API 地址就行。</content:encoded></item><item><title>剑指 Offer 03. 数组中重复的数字</title><link>https://kaitolab.net/blog/leetcode-offer-03/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-03/</guid><description>剑指 Offer 03. 数组中重复的数字</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 03. 数组中重复的数字

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0～n-1 的范围内。数组中某些数字是重复的，但不知道有几个数字重复了，也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

**示例 1：**

```
输入：
[2, 3, 1, 0, 2, 5, 3]
输出：2 或 3 
```

**限制：**

```
2 &lt;= n &lt;= 100000
```

## 代码

**Go：**

```go
func findRepeatNumber(nums []int) int {
    var count [100000]int
    for i:=0;i&lt;len(nums);i++{
        count[nums[i]]++
        if count[nums[i]]&gt;1{
            return nums[i]
        }
    }
    return 0
}
```</content:encoded></item><item><title>剑指 Offer 04. 二维数组中的查找</title><link>https://kaitolab.net/blog/leetcode-offer-04/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-04/</guid><description>剑指 Offer 04. 二维数组中的查找</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 04. 二维数组中的查找

在一个 n \* m 的二维数组中，每一行都按照从左到右 **非递减** 的顺序排序，每一列都按照从上到下 **非递减** 的顺序排序。请完成一个高效的函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。

**示例:**

现有矩阵 matrix 如下：

```
[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
```

给定 target = `5`，返回 `true`。

给定 target = `20`，返回 `false`。

**限制：**

```
0 &lt;= n &lt;= 1000
0 &lt;= m &lt;= 1000
```

## 代码

**Go：**

```go
func findNumberIn2DArray(matrix [][]int, target int) bool {
    var flagx,flagy int
    if len(matrix)==0len(matrix[0])==0{
        return false
    }

    flagx=len(matrix)-1
    flagy=0
    for matrix[flagx][flagy]!=target{
        if matrix[flagx][flagy]&gt;target &amp;&amp; flagx&gt;0{
            flagx--
        }else if matrix[flagx][flagy]&lt;target &amp;&amp; flagy&lt;len(matrix[flagx])-1{
            flagy++
        }else{
            return false
        }
    }
    return true

}
```</content:encoded></item><item><title>剑指 Offer 05. 替换空格</title><link>https://kaitolab.net/blog/leetcode-offer-05/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-05/</guid><description>剑指 Offer 05. 替换空格</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 05. 替换空格

请实现一个函数，把字符串 `s` 中的每个空格替换成&quot;%20&quot;。

**示例 1：**

```
输入：s = &quot;We are happy.&quot;
输出：&quot;We%20are%20happy.&quot;
```

**限制：**

```
0 &lt;= s 的长度 &lt;= 10000
```

## 代码

**Go：**

```go
func replaceSpace(s string) string {
    var news string
    for i:=0;i!=len(s);i++{
        if s[i]!=&apos; &apos;{
            news+=string(s[i])
        }else{
            news+=&quot;%20&quot;
        }
    }
    return news
}
```</content:encoded></item><item><title>剑指 Offer 06. 从尾到头打印链表</title><link>https://kaitolab.net/blog/leetcode-offer-06/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-06/</guid><description>剑指 Offer 06. 从尾到头打印链表</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 06. 从尾到头打印链表

输入一个链表的头节点，从尾到头反过来返回每个节点的值（用数组返回）。

**示例 1：**

```
输入：head = [1,3,2]
输出：[2,3,1]
```

**限制：**

```
0 &lt;= 链表长度 &lt;= 10000
```

## 代码

**Go：**

```go
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */

func reversePrint(head *ListNode) []int {
    if head==nil{
        res:=make([]int,0) 
        return res
    }

    return append(reversePrint(head.Next),head.Val)
}
```</content:encoded></item><item><title>剑指 Offer 09. 用两个栈实现队列</title><link>https://kaitolab.net/blog/leetcode-offer-09/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-09/</guid><description>剑指 Offer 09. 用两个栈实现队列</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 09. 用两个栈实现队列

用两个栈实现一个队列。队列的声明如下，请实现它的两个函数 `appendTail` 和 `deleteHead` ，分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素，`deleteHead` 操作返回 -1 )

**示例 1：**

```
输入：
[&quot;CQueue&quot;,&quot;appendTail&quot;,&quot;deleteHead&quot;,&quot;deleteHead&quot;,&quot;deleteHead&quot;]
[[],[3],[],[],[]]
输出：[null,null,3,-1,-1]
```

**示例 2：**

```
输入：
[&quot;CQueue&quot;,&quot;deleteHead&quot;,&quot;appendTail&quot;,&quot;appendTail&quot;,&quot;deleteHead&quot;,&quot;deleteHead&quot;]
[[],[],[5],[2],[],[]]
输出：[null,-1,null,null,5,2]
```

**提示：**

*   `1 &lt;= values &lt;= 10000`
*   最多会对 `appendTail、deleteHead` 进行 `10000` 次调用

## 代码

**C++：**

```cpp
class CQueue {
private:
    stack&lt;int&gt; inStack, outStack;

public:
    CQueue() {

    }

    void appendTail(int value) {
        inStack.push(value);
    }

    int deleteHead() {
        int a;
        if(outStack.empty())
        {
            while(!inStack.empty())
            {
                outStack.push(inStack.top());
                inStack.pop();
            }
        }
        if(outStack.empty())
            return -1;
        else
        {
            a=outStack.top();
            outStack.pop();
            return a;
        }

    }
};

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue* obj = new CQueue();
 * obj-&gt;appendTail(value);
 * int param_2 = obj-&gt;deleteHead();
 */
```

**Go：**

```go
type CQueue struct {
    inStack, outStack []int
}

//initial
func Constructor() CQueue {
    return CQueue{}
}

func (this *CQueue) AppendTail(value int)  {
    this.inStack=append(this.inStack,value)
}

func (this *CQueue) DeleteHead() int {
    if len(this.outStack)==0{
        for len(this.inStack)!=0 {
            v := this.inStack[len(this.inStack)-1]
            this.inStack = this.inStack[:len(this.inStack)-1]
            this.outStack=append(this.outStack,v)
        }
    }
    if len(this.outStack)==0{
        return -1
    }else {
        w := this.outStack[len(this.outStack)-1]
        this.outStack = this.outStack[:len(this.outStack)-1]
        return w
    }

}

/**
 * Your CQueue object will be instantiated and called as such:
 * obj := Constructor();
 * obj.AppendTail(value);
 * param_2 := obj.DeleteHead();
 */
```</content:encoded></item><item><title>剑指 Offer 10- I. 斐波那契数列</title><link>https://kaitolab.net/blog/leetcode-offer-10-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-10-1/</guid><description>剑指 Offer 10- I. 斐波那契数列</description><pubDate>Fri, 13 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 10- I. 斐波那契数列

写一个函数，输入 `n` ，求斐波那契（Fibonacci）数列的第 `n` 项（即 `F(N)`）。斐波那契数列的定义如下：

```
F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N &gt; 1.
```

斐波那契数列由 0 和 1 开始，之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7（1000000007），如计算初始结果为：1000000008，请返回 1。

**示例 1：**

```
输入：n = 2
输出：1
```

**示例 2：**

```
输入：n = 5
输出：5
```

**提示：**

*   `0 &lt;= n &lt;= 100`

## 代码

**Go：**递归超时，需要采用简单动态规划

```go
func fib(n int) int {
    if n==0{
        return 0
    }else if n==1{
        return 1
    }
    dp:=make([]int,101)
    dp[0]=0
    dp[1]=1
    for i:=2;i&lt;=n;i++{
        dp[i]=(dp[i-1]+dp[i-2])%1000000007
    }
    return dp[n]
}
```</content:encoded></item><item><title>剑指 Offer 10- II. 青蛙跳台阶问题</title><link>https://kaitolab.net/blog/leetcode-offer-10-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-10-2/</guid><description>剑指 Offer 10- II. 青蛙跳台阶问题</description><pubDate>Fri, 13 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 10- II. 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶，也可以跳上2级台阶。求该青蛙跳上一个 `n` 级的台阶总共有多少种跳法。

答案需要取模 1e9+7（1000000007），如计算初始结果为：1000000008，请返回 1。

**示例 1：**

```
输入：n = 2
输出：2
```

**示例 2：**

```
输入：n = 7
输出：21
```

**示例 3：**

```
输入：n = 0
输出：1
```

**提示：**

*   `0 &lt;= n &lt;= 100`

注意：本题与主站 70 题相同：[https://leetcode-cn.com/problems/climbing-stairs/](https://leetcode-cn.com/problems/climbing-stairs/)

## 代码

**Go：**简单动态规划 解法同斐波那契数列

```go
func numWays(n int) int {
    if n==0{
        return 1
    }else if n==1{
        return 1
    }
    dp:=make([]int,101)
    dp[0]=1
    dp[1]=1
    for i:=2;i&lt;=n;i++{
        dp[i]=(dp[i-1]+dp[i-2])%1000000007
    }
    return dp[n]
}
```</content:encoded></item><item><title>剑指 Offer 11. 旋转数组的最小数字</title><link>https://kaitolab.net/blog/leetcode-offer-11/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-11/</guid><description>剑指 Offer 11. 旋转数组的最小数字</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 11. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾，我们称之为数组的旋转。

给你一个可能存在 **重复** 元素值的数组 `numbers` ，它原来是一个升序排列的数组，并按上述情形进行了一次旋转。请返回旋转数组的**最小元素**。例如，数组 `[3,4,5,1,2]` 为 `[1,2,3,4,5]` 的2次旋转，该数组的最小值为 1。

注意，数组 `[a[0], a[1], a[2], ..., a[n-1]]` 旋转一次 的结果为数组 `[a[n-1], a[0], a[1], a[2], ..., a[n-2]]` 。

**示例 1：**

```
输入：numbers = [3,4,5,1,2]
输出：1
```

**示例 2：**

```
输入：numbers = [2,2,2,0,1]
输出：0
```

**提示：**

*   `n == numbers.length`
*   `1 &lt;= n &lt;= 5000`
*   `-5000 &lt;= numbers[i] &lt;= 5000`
*   `numbers` 原来是一个升序排序的数组，并进行了 `1` 至 `n` 次旋转

注意：本题与主站 154 题相同：[https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/)

## 代码

**Go：**二分查找

```go
func minArray(numbers []int) int {
    i:=0
    j:=len(numbers)-1
    for i&lt;=j{
        mid:=i+(j-i)/2
        if numbers[mid]&gt;numbers[j]{
            i=mid+1
        }else if numbers[mid]&lt;numbers[j]{
            j=mid
        }else{
            j--
        }
    }
    return numbers[j+1]
}
```</content:encoded></item><item><title>剑指 Offer 24. 反转链表</title><link>https://kaitolab.net/blog/leetcode-offer-24/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-24/</guid><description>剑指 Offer 24. 反转链表</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 24. 反转链表

请实现 `copyRandomList` 函数，复制一个复杂链表。在复杂链表中，每个节点除了有一个 `next` 指针指向下一个节点，还有一个 `random` 指针指向链表中的任意节点或者 `null`。

**示例 1：**

```
输入：head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出：[[7,null],[13,0],[11,4],[10,2],[1,0]]
```

**示例 2：**

```
输入：head = [[1,1],[2,1]]
输出：[[1,1],[2,1]]
```

**示例 3：**

```
输入：head = [[3,null],[3,0],[3,null]]
输出：[[3,null],[3,0],[3,null]]
```

**示例 4：**

```
输入：head = []
输出：[]
解释：给定的链表为空（空指针），因此返回 null。
```

**提示：**

*   `-10000 &lt;= Node.val &lt;= 10000`
*   `Node.random` 为空（null）或指向链表中的节点。
*   节点数目不超过 1000 。

## 代码

**Go：**

```go
/**
 * Definition for a Node.
 * type Node struct {
 *     Val int
 *     Next *Node
 *     Random *Node
 * }
 */

func copyRandomList(head *Node) *Node {
    if head==nil{
        return nil
    }
    var p,q  *Node
    for p := head; p != nil; p = p.Next.Next {
        p.Next = &amp;Node{Val: p.Val, Next: p.Next}
    }

    for p=head;p!=nil;p=p.Next.Next{
        if p.Random!=nil{
            p.Next.Random=p.Random.Next
        }        

    }
    newhead:=head.Next
    q=head

    for p=head.Next.Next;p!=nil;p=p.Next.Next{
        q.Next.Next=p.Next;
        q.Next=p
        q=p;
    } 
    q.Next=nil
    return newhead

}
```</content:encoded></item><item><title>剑指 Offer 26. 树的子结构</title><link>https://kaitolab.net/blog/leetcode-offer-26/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-26/</guid><description>剑指 Offer 26. 树的子结构</description><pubDate>Fri, 13 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 26. 树的子结构

输入两棵二叉树A和B，判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构， 即 A中有出现和B相同的结构和节点值。

例如:给定的树 A:

```
    3
   / \
   4  5
  / \
 1  2
```

给定的树 B：

```
   4 
  /
 1
```

返回 true，因为 B 与 A 的一个子树拥有相同的结构和节点值。

**示例 1：**

```
输入：A = [1,2,3], B = [3,1]
输出：false
```

**示例 2：**

```
输入：A = [3,4,5,1,2], B = [4,1]
输出：true
```

**限制：**

```
0 &lt;= 节点个数 &lt;= 10000
```

## 代码

**Go：** DFS深度优先搜索

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func isSameTree(A *TreeNode, B *TreeNode) bool {
    if A==nil&amp;&amp;B!=nilA==nil&amp;&amp;B!=nil{
        return false
    }else if A!=nil&amp;&amp;B!=nil{
        if A.Val!=B.Val{
            return false
        }else if isSameTree(A.Left,B.Left)==falseisSameTree(A.Right,B.Right)==false{
            return false
        }
    }
    return true
}

func DFS(root *TreeNode,B *TreeNode) bool{
    if root==nil{
        return false
    }
    if isSameTree(root,B){
        return true
    }else if root.Left!=nil&amp;&amp;DFS(root.Left,B){
        return true
    }else if root.Right!=nil&amp;&amp;DFS(root.Right,B){
        return true
    }
    return false
}

func isSubStructure(A *TreeNode, B *TreeNode) bool {
    if B==nil{
        return false
    }
    return DFS(A,B)
}
```

**优化后的代码：**时间不变，内存占用减少

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */

func compare(A *TreeNode, B *TreeNode) bool{
    if B == nil{
        return true
    }
    if A == nil{
        return false
    }
    return A.Val == B.Val &amp;&amp; compare(A.Left, B.Left) &amp;&amp; compare(A.Right, B.Right)
}

func isSubStructure(A *TreeNode, B *TreeNode) bool {
    if(A == nil  B == nil) {
        return false
    }
    return compare(A, B)  isSubStructure(A.Left, B)  isSubStructure(A.Right, B)
}
```</content:encoded></item><item><title>剑指 Offer 27. 二叉树的镜像</title><link>https://kaitolab.net/blog/leetcode-offer-27/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-27/</guid><description>剑指 Offer 27. 二叉树的镜像</description><pubDate>Fri, 13 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 27. 二叉树的镜像

请完成一个函数，输入一个二叉树，该函数输出它的镜像。

例如输入：

```
    4
   / \
  2   7
 / \  / \
1  3 6   9
```

镜像输出：

```
    4
   /  \
  7   2
 / \  / \
 9  6 3  1
```

**示例 1：**

```
输入：root = [4,2,7,1,3,6,9]
输出：[4,7,2,9,6,3,1]
```

**限制：**

```
0 &lt;= 节点个数 &lt;= 1000
```

注意：本题与主站 226 题相同：[https://leetcode-cn.com/problems/invert-binary-tree/](https://leetcode-cn.com/problems/invert-binary-tree/)

## 代码

**Go：**

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func exchangeNode(root *TreeNode){
    if root!=nil{
        root.Left,root.Right=root.Right,root.Left
        exchangeNode(root.Left)
        exchangeNode(root.Right)
    }
}

func mirrorTree(root *TreeNode) *TreeNode {
    exchangeNode(root)
    return root
}
```</content:encoded></item><item><title>剑指 Offer 28. 对称的二叉树</title><link>https://kaitolab.net/blog/leetcode-offer-28/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-28/</guid><description>剑指 Offer 28. 对称的二叉树</description><pubDate>Fri, 13 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 28. 对称的二叉树

请实现一个函数，用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样，那么它是对称的。

例如，二叉树 \[1,2,2,3,4,4,3\] 是对称的。

```
     1
    / \
   2   2
  / \ / \
 3  4 4  3
```

但是下面这个 \[1,2,2,null,3,null,3\] 则不是镜像对称的:

```
    1
   / \
  2   2
   \   \
    3   3
```

**示例 1：**

```
输入：root = [1,2,2,3,4,4,3]
输出：true
```

**示例 2：**

```
输入：root = [1,2,2,null,3,null,3]
输出：false
```

**限制：**

```
0 &lt;= 节点个数 &lt;= 1000
```

注意：本题与主站 101 题相同：[https://leetcode-cn.com/problems/symmetric-tree/](https://leetcode-cn.com/problems/symmetric-tree/)

## 代码

**Go：**递归判断左右子树是否对称

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func compareNode(A *TreeNode,B *TreeNode) bool{
    if A==nil&amp;&amp;B==nil{
        return true
    }
    if A==nilB==nilA.Val!=B.Val{
        return false
    }
    return compareNode(A.Left,B.Right)&amp;&amp;compareNode(A.Right,B.Left)
}

func isSymmetric(root *TreeNode) bool {
    if root==nil{
        return true
    }else{
        return compareNode(root.Left,root.Right)
    }
}
```</content:encoded></item><item><title>剑指 Offer 30. 包含min函数的栈</title><link>https://kaitolab.net/blog/leetcode-offer-30/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-30/</guid><description>剑指 Offer 30. 包含min函数的栈</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 30. 包含min函数的栈

定义栈的数据结构，请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中，调用 min、push 及 pop 的时间复杂度都是 O(1)。

**示例:**

```
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --&gt; 返回 -3.
minStack.pop();
minStack.top();      --&gt; 返回 0.
minStack.min();   --&gt; 返回 -2.
```

**提示：**

1.  各函数的调用总次数不超过 20000 次

## 代码

**C++：**

```cpp
class MinStack {
private:
    stack&lt;int&gt; mstack;
    int minnum[20000];
    int length;
public:
    /** initialize your data structure here. */
    MinStack() {
        minnum[0]=0;
        length=0;
    }

    void push(int x) {
        if(length==0)
        {
            minnum[length+1]=x;
        }
        else
        {
            if(minnum[length]&lt;x)
                minnum[length+1]=minnum[length];
            else
                minnum[length+1]=x;
        }
        length++;
        mstack.push(x);
    }

    void pop() {
        mstack.pop();
        length--;
    }

    int top() {
        return mstack.top();
    }

    int min() {
        return minnum[length];
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack* obj = new MinStack();
 * obj-&gt;push(x);
 * obj-&gt;pop();
 * int param_3 = obj-&gt;top();
 * int param_4 = obj-&gt;min();
 */
```

**Go：**

```go
type MinStack struct {
    min,data []int
}

/** initialize your data structure here. */
func Constructor() MinStack {
    return MinStack{}
}

func (this *MinStack) Push(x int)  {
    if len(this.data)==0  x&lt;this.min[len(this.min)-1]{
        this.min=append(this.min,x)
    }else{
        this.min=append(this.min,this.min[len(this.min)-1])
    }
    this.data=append(this.data,x)
}

func (this *MinStack) Pop()  {
    if len(this.data)!=0{
        this.data=this.data[:len(this.data)-1]
        this.min=this.min[:len(this.min)-1]
    }
}

func (this *MinStack) Top() int {
    return this.data[len(this.data)-1]
}

func (this *MinStack) Min() int {
    return this.min[len(this.min)-1]
}

/**
 * Your MinStack object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Push(x);
 * obj.Pop();
 * param_3 := obj.Top();
 * param_4 := obj.Min();
 */
```</content:encoded></item><item><title>剑指 Offer 32 - I. 从上到下打印二叉树</title><link>https://kaitolab.net/blog/leetcode-offer-32-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-32-1/</guid><description>剑指 Offer 32 - I. 从上到下打印二叉树</description><pubDate>Tue, 10 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 32 - I. 从上到下打印二叉树

从上到下打印出二叉树的每个节点，同一层的节点按照从左到右的顺序打印。

例如: 给定二叉树: `[3,9,20,null,null,15,7]`,

```
    3
   / \
  9  20
    /  \
   15   7
```

返回：

```
[3,9,20,15,7]
```

**提示：**

1.  `节点总数 &lt;= 1000`

## 代码

**Go：**层序遍历，借助队列

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func levelOrder(root *TreeNode) []int {
    res:=make([]int,0)
    if root==nil {
        return nil
    }
    queue:=make([]*TreeNode,0)
    queue=append(queue,root)
    for len(queue)&gt;0{
        node:=queue[0]
        res=append(res,node.Val)
        if node.Left!=nil{
            queue=append(queue,node.Left)
        }
        if node.Right!=nil{
            queue=append(queue,node.Right)
        }
        queue=queue[1:]
    }
    return res
}
```</content:encoded></item><item><title>剑指 Offer 32 - II. 从上到下打印二叉树 II</title><link>https://kaitolab.net/blog/leetcode-offer-32-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-32-2/</guid><description>剑指 Offer 32 - II. 从上到下打印二叉树 II</description><pubDate>Tue, 10 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 32 - II. 从上到下打印二叉树 II

从上到下按层打印二叉树，同一层的节点按从左到右的顺序打印，每一层打印到一行。

例如: 给定二叉树: `[3,9,20,null,null,15,7]`,

```
    3
   / \
  9  20
    /  \
   15   7
```

返回其层次遍历结果：

```
[
  [3],
  [9,20],
  [15,7]
]
```

**提示：**

1.  `节点总数 &lt;= 1000`

注意：本题与主站 102 题相同：[https://leetcode-cn.com/problems/binary-tree-level-order-traversal/](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/)

## 代码

**Go：**层次遍历，每次的旧队列是一层，新队列是下一层

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func levelOrder(root *TreeNode) [][]int {
    res:=make([][]int,0)
    if root==nil {
        return nil
    }
    oldqueue:=make([]*TreeNode,0)
    oldqueue=append(oldqueue,root)
    for len(oldqueue)&gt;0{
        newqueue:=make([]*TreeNode,0)
        tmp:=make([]int,0)
        for len(oldqueue)&gt;0{
            tmp=append(tmp,oldqueue[0].Val)
            if oldqueue[0].Left!=nil{
                newqueue=append(newqueue,oldqueue[0].Left)
            }
            if oldqueue[0].Right!=nil{
                newqueue=append(newqueue,oldqueue[0].Right)
            }
            oldqueue=oldqueue[1:]
        }
        res=append(res,tmp)
        oldqueue=newqueue
    }
    return res
}
```</content:encoded></item><item><title>剑指 Offer 32 - III. 从上到下打印二叉树 III</title><link>https://kaitolab.net/blog/leetcode-offer-32-3/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-32-3/</guid><description>剑指 Offer 32 - III. 从上到下打印二叉树 III</description><pubDate>Tue, 10 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 32 - III. 从上到下打印二叉树 III

请实现一个函数按照之字形顺序打印二叉树，即第一行按照从左到右的顺序打印，第二层按照从右到左的顺序打印，第三行再按照从左到右的顺序打印，其他行以此类推。

例如: 给定二叉树: `[3,9,20,null,null,15,7]`,

```
    3
   / \
  9  20
    /  \
   15   7
```

返回其层次遍历结果：

```
[
  [3],
  [20,9],
  [15,7]
]
```

**提示：**

1.  `节点总数 &lt;= 1000`

## 代码

**Go：**层次遍历，之字形说明栈和队列交错输出

```go
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func levelOrder(root *TreeNode) [][]int {
    flag:=1
    res:=make([][]int,0)
    if root==nil {
        return nil
    }
    oldqueue:=make([]*TreeNode,0)
    oldqueue=append(oldqueue,root)
    for len(oldqueue)&gt;0{
        newqueue:=make([]*TreeNode,0)
        tmp:=make([]int,0)
        for len(oldqueue)&gt;0{
            tmp=append(tmp,oldqueue[0].Val)
            if oldqueue[0].Left!=nil{
                newqueue=append(newqueue,oldqueue[0].Left)
            }
            if oldqueue[0].Right!=nil{
                newqueue=append(newqueue,oldqueue[0].Right)
            }
            oldqueue=oldqueue[1:]
        }
        if flag==1{
            res=append(res,tmp)
        }else{
            v:=make([]int,0)
            for len(tmp)&gt;0{
                v=append(v,tmp[len(tmp)-1])
                tmp=tmp[:len(tmp)-1]
            }
            res=append(res,v)
        }
        flag=-flag
        oldqueue=newqueue
    }
    return res
}
```</content:encoded></item><item><title>剑指 Offer 35. 复杂链表的复制</title><link>https://kaitolab.net/blog/leetcode-offer-35/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-35/</guid><description>剑指 Offer 35. 复杂链表的复制</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 35. 复杂链表的复制

定义一个函数，输入一个链表的头节点，反转该链表并输出反转后链表的头节点。

**示例:**

```
输入: 1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;NULL
输出: 5-&gt;4-&gt;3-&gt;2-&gt;1-&gt;NULL
```

**限制：**

```
0 &lt;= 节点个数 &lt;= 5000
```

## 代码

**Go：**

```go
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reverseList(head *ListNode) *ListNode {
    var p,q *ListNode
    p=head
    if head==nil{
        return nil
    }
    for p.Next!=nil{
        q=head
        head=p.Next
        p.Next=head.Next
        head.Next=q
    }
    return head
}
```</content:encoded></item><item><title>剑指 Offer 50. 第一个只出现一次的字符</title><link>https://kaitolab.net/blog/leetcode-offer-50/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-50/</guid><description>剑指 Offer 50. 第一个只出现一次的字符</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 50. 第一个只出现一次的字符

在字符串 s 中找出第一个只出现一次的字符。如果没有，返回一个单空格。 s 只包含小写字母。

**示例 1:**

```
输入：s = &quot;abaccdeff&quot;
输出：&apos;b&apos;
```

**示例 2:**

```
输入：s = &quot;&quot; 
输出：&apos; &apos;
```

**限制：**

```
0 &lt;= s 的长度 &lt;= 50000
```

## 代码

**Go：**哈希表映射，map没有顺序

```go
func firstUniqChar(s string) byte {
    chars := make(map[byte]int)

    for i:=0;i&lt;len(s);i++{
        chars[s[i]]++
    }
    for i:=0;i&lt;len(s);i++{
        if chars[s[i]]==1{
            return s[i]
        }
    }
    return &apos; &apos;
}
```</content:encoded></item><item><title>剑指 Offer 53 - I. 在排序数组中查找数字 I</title><link>https://kaitolab.net/blog/leetcode-offer-53-1/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-53-1/</guid><description>剑指 Offer 53 - I. 在排序数组中查找数字 I</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

**示例 1:**

```
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
```

**示例 2:**

```
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
```

**提示：**

*   `0 &lt;= nums.length &lt;= 105`
*   `-109 &lt;= nums[i] &lt;= 109`
*   `nums` 是一个非递减数组
*   `-109 &lt;= target &lt;= 109`

## 代码

**Go：**

```go
func search(nums []int, target int) int {
    var i,j,mid,count int

    i=0;
    j=len(nums)-1
    count=0
    for i&lt;=j{
        mid=(i+j)/2
        if target==nums[mid]{
            i=mid
            j=mid
            for ;i&gt;=0&amp;&amp;nums[i]==target;i--{
                count++
            }
            for ;j&lt;=len(nums)-1&amp;&amp;nums[j]==target;j++{
                count++
            }
            return count-1
        }else if target&gt;nums[mid] {
            i=mid+1
        }else {
            j=mid-1
        }
    }
    return 0;
}
```</content:encoded></item><item><title>剑指 Offer 53 - II. 0～n-1中缺失的数字</title><link>https://kaitolab.net/blog/leetcode-offer-53-2/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-53-2/</guid><description>剑指 Offer 53 - II. 0～n-1中缺失的数字</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 53 - II. 0～n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的，并且每个数字都在范围0～n-1之内。在范围0～n-1内的n个数字中有且只有一个数字不在该数组中，请找出这个数字。

**示例 1:**

```
输入: [0,1,3]
输出: 2
```

**示例 2:**

```
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
```

**限制：**

```
1 &lt;= 数组长度 &lt;= 10000
```

## 代码

**Go：**

```go
func missingNumber(nums []int) int {
    var count int=0
    for i:=0;i&lt;len(nums);i++{
        if nums[i]==count{
            count++
        }
    }
    return count
}
```</content:encoded></item><item><title>剑指 Offer 58 - II. 左旋转字符串</title><link>https://kaitolab.net/blog/leetcode-offer-58/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-58/</guid><description>剑指 Offer 58 - II. 左旋转字符串</description><pubDate>Mon, 09 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 58 - II. 左旋转字符串

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如，输入字符串&quot;abcdefg&quot;和数字2，该函数将返回左旋转两位得到的结果&quot;cdefgab&quot;。

**示例 1：**

```
输入: s = &quot;abcdefg&quot;, k = 2
输出: &quot;cdefgab&quot;
```

**示例 2：**

```
输入: s = &quot;lrloseumgh&quot;, k = 6
输出: &quot;umghlrlose&quot;
```

**限制：**

*   `1 &lt;= k &lt; s.length &lt;= 10000`

## 代码

**Go：**

```go
func reverseLeftWords(s string, n int) string {
    var a string
    a+=s[n:]
    a+=s[0:n]
    return a
}
```</content:encoded></item><item><title>剑指 Offer 63. 股票的最大利润</title><link>https://kaitolab.net/blog/leetcode-offer-63/</link><guid isPermaLink="true">https://kaitolab.net/blog/leetcode-offer-63/</guid><description>剑指 Offer 63. 股票的最大利润</description><pubDate>Fri, 13 Jan 2023 16:00:00 GMT</pubDate><content:encoded>## 题目

剑指 Offer 63. 股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中，请问买卖该股票一次可能获得的最大利润是多少？

**示例 1:**

```
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
```

**示例 2:**

```
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
```

**限制：**

```
0 &lt;= 数组长度 &lt;= 10^5
```

**注意：**本题与主站 121 题相同：[https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)

## 代码

**Go：**暴力法

```go
func maxProfit(prices []int) int {
    dp:=make([]int,100001)
    n:=len(prices)
    dp[0]=0;
    for i:=1;i&lt;n;i++{
        dp[i]=prices[i]-prices[i-1]
    }
    max:=0
    for i:=0;i&lt;n;i++{
        tmp:=0
        for j:=i+1;j&lt;n;j++{
            tmp+=dp[j]
            if tmp&gt;max{
                max=tmp
            }
        }
    }
    return max
}
```

**如何进一步优化**

动态规划五部曲分析如下：

1.确定dp数组（dp table）以及下标的含义 dp\[i\]\[0\] 表示第i天持有股票所得最多现金

一开始现金是0，那么加入第i天买入股票现金就是 -prices\[i\]， 这是一个负数。

dp\[i\]\[1\] 表示第i天不持有股票所得最多现金

注意这里说的是“持有”，“持有”不代表就是当天“买入”！也有可能是昨天就买入了，今天保持持有的状态

2.确定递推公式 如果第i天持有股票即dp\[i\]\[0\]， 那么可以由两个状态推出来

· 第i-1天就持有股票，那么就保持现状，所得现金就是昨天持有股票的所得现金 即：dp\[i - 1\]\[0\] · 第i天买入股票，所得现金就是买入今天的股票后所得现金即：-prices\[i\] 那么dp\[i\]\[0\]应该选所得现金最大的，所以dp\[i\]\[0\] = max(dp\[i - 1\]\[0\], -prices\[i\]);

如果第i天不持有股票即dp\[i\]\[1\]， 也可以由两个状态推出来

· 第i-1天就不持有股票，那么就保持现状，所得现金就是昨天不持有股票的所得现金 即：dp\[i - 1\]\[1\] · 第i天卖出股票，所得现金就是按照今天股票佳价格卖出后所得现金即：prices\[i\] + dp\[i - 1\]\[0\] 同样dp\[i\]\[1\]取最大的，dp\[i\]\[1\] = max(dp\[i - 1\]\[1\], prices\[i\] + dp\[i - 1\]\[0\]);

3.dp数组如何初始化 由递推公式 dp\[i\]\[0\] = max(dp\[i - 1\]\[0\], -prices\[i\]); 和 dp\[i\]\[1\] = max(dp\[i - 1\]\[1\], prices\[i\] + dp\[i - 1\]\[0\]);可以看出

其基础都是要从dp\[0\]\[0\]和dp\[0\]\[1\]推导出来。

那么dp\[0\]\[0\]表示第0天持有股票，此时的持有股票就一定是买入股票了，因为不可能有前一天推出来，所以dp\[0\]\[0\] -= prices\[0\];

dp\[0\]\[1\]表示第0天不持有股票，不持有股票那么现金就是0，所以dp\[0\]\[1\] = 0;

4.确定遍历顺序 从递推公式可以看出dp\[i\]都是有dp\[i - 1\]推导出来的，那么一定是从前向后遍历。

5.举例推导dp数组

**代码如下**

```go
func maxProfit(prices []int) int {
    length:=len(prices)
    if length==0{return 0}
    dp:=make([][]int,length)
    for i:=0;i&lt;length;i++{
        dp[i]=make([]int,2)
    }

    dp[0][0]=-prices[0]
    dp[0][1]=0
    for i:=1;i&lt;length;i++{
        dp[i][0]=max(dp[i-1][0],-prices[i])
        dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
    }
    return dp[length-1][1]
}

func max(a,b int)int {
    if a&gt;b{
        return a 
    }
    return b 
}
```</content:encoded></item><item><title>Markdown Style Guide</title><link>https://kaitolab.net/blog/markdown-style-guide/</link><guid isPermaLink="true">https://kaitolab.net/blog/markdown-style-guide/</guid><description>Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.</description><pubDate>Tue, 18 Jun 2024 16:00:00 GMT</pubDate><content:encoded>Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.

## Headings

The following HTML `&lt;h1&gt;`—`&lt;h6&gt;` elements represent six levels of section headings. `&lt;h1&gt;` is the highest section level while `&lt;h6&gt;` is the lowest.

# H1

## H2

### H3

#### H4

##### H5

###### H6

## Paragraph

Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.

Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.

## Images

### Syntax

```markdown
![Alt text](./full/or/relative/path/of/image)
```

### Output

![blog placeholder](../../assets/blog-placeholder-about.jpg)

## Blockquotes

The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations.

### Blockquote without attribution

#### Syntax

```markdown
&gt; Tiam, ad mint andaepu dandae nostion secatur sequo quae.  
&gt; **Note** that you can use _Markdown syntax_ within a blockquote.
```

#### Output

&gt; Tiam, ad mint andaepu dandae nostion secatur sequo quae.  
&gt; **Note** that you can use _Markdown syntax_ within a blockquote.

### Blockquote with attribution

#### Syntax

```markdown
&gt; Don&apos;t communicate by sharing memory, share memory by communicating.&lt;br&gt;
&gt; — &lt;cite&gt;Rob Pike[^1]&lt;/cite&gt;
```

#### Output

&gt; Don&apos;t communicate by sharing memory, share memory by communicating.&lt;br&gt;
&gt; — &lt;cite&gt;Rob Pike[^1]&lt;/cite&gt;

[^1]: The above quote is excerpted from Rob Pike&apos;s [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.

## Tables

### Syntax

```markdown
| Italics   | Bold     | Code   |
| --------- | -------- | ------ |
| _italics_ | **bold** | `code` |
```

### Output

| Italics   | Bold     | Code   |
| --------- | -------- | ------ |
| _italics_ | **bold** | `code` |

## Code Blocks

### Syntax

we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntax, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash

````markdown
```html
&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;title&gt;Example HTML5 Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;Test&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;
```
````

### Output

```html
&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;title&gt;Example HTML5 Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;Test&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;
```

## List Types

### Ordered List

#### Syntax

```markdown
1. First item
2. Second item
3. Third item
```

#### Output

1. First item
2. Second item
3. Third item

### Unordered List

#### Syntax

```markdown
- List item
- Another item
- And another item
```

#### Output

- List item
- Another item
- And another item

### Nested list

#### Syntax

```markdown
- Fruit
  - Apple
  - Orange
  - Banana
- Dairy
  - Milk
  - Cheese
```

#### Output

- Fruit
  - Apple
  - Orange
  - Banana
- Dairy
  - Milk
  - Cheese

## Other Elements — abbr, sub, sup, kbd, mark

### Syntax

```markdown
&lt;abbr title=&quot;Graphics Interchange Format&quot;&gt;GIF&lt;/abbr&gt; is a bitmap image format.

H&lt;sub&gt;2&lt;/sub&gt;O

X&lt;sup&gt;n&lt;/sup&gt; + Y&lt;sup&gt;n&lt;/sup&gt; = Z&lt;sup&gt;n&lt;/sup&gt;

Press &lt;kbd&gt;CTRL&lt;/kbd&gt; + &lt;kbd&gt;ALT&lt;/kbd&gt; + &lt;kbd&gt;Delete&lt;/kbd&gt; to end the session.

Most &lt;mark&gt;salamanders&lt;/mark&gt; are nocturnal, and hunt for insects, worms, and other small creatures.
```

### Output

&lt;abbr title=&quot;Graphics Interchange Format&quot;&gt;GIF&lt;/abbr&gt; is a bitmap image format.

H&lt;sub&gt;2&lt;/sub&gt;O

X&lt;sup&gt;n&lt;/sup&gt; + Y&lt;sup&gt;n&lt;/sup&gt; = Z&lt;sup&gt;n&lt;/sup&gt;

Press &lt;kbd&gt;CTRL&lt;/kbd&gt; + &lt;kbd&gt;ALT&lt;/kbd&gt; + &lt;kbd&gt;Delete&lt;/kbd&gt; to end the session.

Most &lt;mark&gt;salamanders&lt;/mark&gt; are nocturnal, and hunt for insects, worms, and other small creatures.</content:encoded></item><item><title>基于GitHub代码托管的团队合作开发常用git操作</title><link>https://kaitolab.net/blog/program-talk-git/</link><guid isPermaLink="true">https://kaitolab.net/blog/program-talk-git/</guid><description>基于GitHub代码托管的团队协作开发流程：Git常用操作与分支管理实践</description><pubDate>Wed, 08 Feb 2023 16:00:00 GMT</pubDate><content:encoded>## 基本git命令

### 查看仓库名

git remote

### 查看推送至仓库的用户名和邮箱

git config user.name git config user.email

### 设置推送至仓库的用户名和邮箱

git config --global user.email &quot;you@example.com&quot;

git config --global user.name &quot;Your Name&quot;

–global 对当前用户所有仓库有效

–local 只对某个仓库有效

–system 对系统所有登录的用户有效

### 显示config配置

git config --list

### 刷新远程仓库分支

git fetch

### 查看仓库名对应的远程仓库的地址

git remote -v

### git远程仓库更换名称

git remote set-url origin 新的远程仓库地址

### 上传到远程仓库命令：

git push 仓库名 分支名

//默认是：git push origin master

### 显示上一次提交之前工作目录与git仓库之间的差异

git diff HEAD^

// 在git pull后，可以通过git diff HEAD^ 来查看拉下来的文件有那些具体的修改。

### git撤销本次pull

git merge --abort

// 如果不小心pull了代码，并且有冲突，不想解决。想要放弃本次pull就使用这个命令。

### 查看所有分支的所有操作记录

git reflog

### 合并到当前所在分支

如：git merge winne

// 把本地winne分支代码合并到当前master分支

如：git pull origin winne

// 把远程仓库winne分支代码合并到当前master分支

## 团队开发clone项目和提交项目

为了让同组开发人员也能把github上的资源下载下来之后，他修改的版本也能够同步到远程仓库中，那么就要在这个仓库上添加合伙人（collaborator）： 此步骤是在github上操作，找到 collaborator的设置地方，然后填好合伙人的用户名设置好，然后点击添加。之后他就会收到一个邀请信息，通过之后就能一同同步更新这个开发项目了。 git status :查看当前状态下的文件变化

1.  组员要把项目先克隆下来：git clone 地址
2.  安装需要的依赖项，运行项目，参与项目开发: npm install
3.  新建并切换到新分支（因为人多了不可能都在主分支master开发）：

## 多人协作解决代码上的冲突问题

在多人协作的时候，每次进行版本的push都要先同步一下自己本地项目和远程仓库上的最新版本代码，避免出现代码冲突问题。

使用 git pull origin 分支名 （把想要的分支内容拉取到本地）

1.  拉取github仓库代码同步合并到本地文件：git pull origin 分支名
2.  然后直接到本地文件中进行合并代码后的冲突代码的取舍（编辑器工具有智能提示，所以很容易进行对比，手动解决冲突）
3.  最后就可以把完好的没有冲突的代码提交同步到github了
4.  本地开发代码关联新创建的远程仓库</content:encoded></item><item><title>ROS2入门篇</title><link>https://kaitolab.net/blog/ros2-intro/</link><guid isPermaLink="true">https://kaitolab.net/blog/ros2-intro/</guid><description>ROS2机器人操作系统入门指南：核心概念、架构设计与常用CLI工具</description><pubDate>Thu, 27 Apr 2023 16:00:00 GMT</pubDate><content:encoded># ROS2入门篇

## 架构图

![img](ros2入门篇/v2-471abbce0a08b249637dc603f56dc9cf_b.png)

![image-20220602204152352](ros2入门篇/image-20220602204152352.png)

## 概念简介

### 客户端库 RCL

RCL（ROS Client Library）ROS客户端库，其实就是ROS的一种API，提供了对ROS话题、服务、参数、Action等接口。

### 中间件

ROS1的中间件是ROS组织自己基于TCP/UDP机制建立的。

ROS2采用了第三方的DDS作为中间件，将DDS服务接口进行了一层抽象，保证了上层应用层调用接口的统一性。

ROS2为每家DDS供应商都开发了对应的DDS\_Interface即DDS接口层，然后通过DDS Abstract抽象层RMW(**R**OS **M**iddle**w**are Interface)来统一DDS的API。

## 第一个节点的编写及编译

### 编写

first\_node.cpp

```c++
#include &quot;rclcpp/rclcpp.hpp&quot;

int main(int argc, char **argv)
{
    // 调用rclcpp的初始化函数
    rclcpp::init(argc, argv);
    // 调用rclcpp的循环运行我们创建的first_node节点
    rclcpp::spin(std::make_shared&lt;rclcpp::Node&gt;(&quot;first_node&quot;));
    return 0;
}
```

CmakLists.txt

```cmake
cmake_minimum_required(VERSION 3.22)
project(first_node)

find_package(rclcpp REQUIRED)
add_executable(first_node first_ros2_node.cpp)
target_link_libraries(first_node rclcpp::rclcpp)
```

### Cmake

创建一个新的目录build，运行cmake并进行编译

```shell
mkdir build
cd build
```

运行cmake指令，`..`代表到上级目录找`CMakeLists.txt`

```
cmake ..
```

运行完cmake你应该可以在build目录下看到cmake自动生成的Makefile了，接着就可以运行make指令进行编译

```
make
```

运行完上面的指令，就可以在build目录下发现`first_node`节点了

### Colcon

只编译一个包

```
colcon build --packages-select YOUR_PKG_NAME 
```

不编译测试单元

```
colcon build --packages-select YOUR_PKG_NAME  --cmake-args -DBUILD_TESTING=0
```

运行编译的包的测试

```
colcon test
```

允许通过更改src下的部分文件来改变install（重要）

每次调整 python 脚本时都不必重新build了

```
colcon build --symlink-install
```

你可能需要一个梯子翻个墙：[点击这里试试小鱼用了很多年的梯子](https://portal.shadowsocks.nz/aff.php?aff=41638)

## 节点介绍

### 交互

节点与节点之间就必须要通信了，那他们之间该如何通信呢？ROS2早已为你准备好了一共四种通信方式:

*   话题-topics
    
*   服务-services
    
*   动作-Action
    
*   参数-parameters
    
    ![Nodes-TopicandService](ros2入门篇/Nodes-TopicandService.webp)

### 节点相关的CLI

运行节点(常用)

```
ros2 run &lt;package_name&gt; &lt;executable_name&gt;
```

查看节点列表(常用)：

```
ros2 node list
```

查看节点信息(常用)：

```
ros2 node info &lt;node_name&gt;
```

重映射节点名称

```
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle
```

运行节点时设置参数

```
ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10
```</content:encoded></item><item><title>从 SSH 到 Webhook：让服务器自己部署自己</title><link>https://kaitolab.net/blog/webhook-self-deploy/</link><guid isPermaLink="true">https://kaitolab.net/blog/webhook-self-deploy/</guid><description>经历了 SSH 端口暴露、5000 条 GitHub IP 白名单、密钥格式踩坑后，最终用 Webhook 方案让服务器自主拉取和部署，顺便踩了三个新坑。</description><pubDate>Mon, 08 Jun 2026 16:00:00 GMT</pubDate><content:encoded>## 起点：SSH 推送方案

之前搭建了 GitHub Actions 自动部署到阿里云，架构很直接：CI 构建 → tar 打包 → SSH 传到服务器 → 解压 → 重载 Nginx。

这套方案跑了一段时间，直到我做了一件事：**把 SSH 端口限制为本机 IP**。

然后 CI 就挂了。

## 问题一：IP 白名单不可行

GitHub Actions 跑在微软云上，IP 段是动态的。我查了一下 GitHub 官方 API，Actions 的 IPv4 段有 **5218 条**。阿里云安全组单条规则上限 100 条，根本加不完。

即使能加完，维护成本也极高——GitHub 不定期增删 IP，每次变更都要手动同步。

## 问题二：SSH 密钥格式坑

在排查过程中还发现一个隐藏问题：GitHub Secrets 中存储的 SSH 私钥，通过 `echo &quot;$SSH_KEY&quot; &gt; file` 写入时会**丢失末尾换行符**，导致 SSH 报 `Load key ... invalid format`。

这玩意儿排查起来极其痛苦——本地测是好的，CI 里就挂。最终用 `printf &apos;%s\n&apos;` 替代 `echo` 解决，但心理上已经对 SSH 方案产生了不信任。

## 换个思路：让服务器主动拉取

与其让 CI 费劲巴拉地推送到服务器，不如反过来：

```
GitHub push → CI 发一个 HTTP POST → 服务器收到 → git pull → build → deploy
```

CI 只负责**通知**，服务器自己完成**拉取和部署**。好处：

- **不需要 SSH**：一个 HTTP 请求就够了
- **不需要开端口**：Nginx 已经在监听 80/443
- **安全组不用改**：现有的 Web 端口复用
- **服务器完全掌控部署逻辑**：不在 CI 里写复杂的 shell

## 实现

### 1. 服务端：新增 `/api/deploy` 端点

在之前写的 API Bridge 服务（Express, 端口 3001）上加了一个 webhook 端点，带密钥校验：

```javascript
app.post(&apos;/api/deploy&apos;, async (req, res) =&gt; {
  const { secret } = req.body;

  if (secret !== DEPLOY_SECRET) {
    return res.status(403).json({ error: &apos;密钥错误&apos; });
  }

  // 先返回 200，部署在后台异步执行
  res.json({ status: &apos;deploying&apos; });

  runDeploy().catch((err) =&gt; {
    console.error(&apos;[deploy] 部署失败:&apos;, err.message);
  });
});
```

部署逻辑拆成清晰的五步：

```javascript
async function runDeploy() {
  // 1. 拉取最新代码
  run(&apos;git fetch origin main &amp;&amp; git reset --hard origin/main&apos;, REPO_DIR);

  // 2. 先更新 API Bridge 自身依赖（只装不重启，避免杀进程）
  run(&apos;npm install --omit=dev&apos;, &apos;/opt/api-bridge&apos;);

  // 3. 安装博客依赖 + 构建
  run(&apos;npm ci&apos;, REPO_DIR);
  run(&apos;BASE_URL=/ npm run build&apos;, REPO_DIR);  // systemd 环境需显式传入

  // 4. Nginx 直接指向构建目录，仅需 SELinux 修复 + 重载
  run(&apos;restorecon -R /var/www/kaitohub/dist&apos;, REPO_DIR);
  run(&apos;systemctl reload nginx&apos;, REPO_DIR);

  // 5. 最后才重启 API Bridge（部署全部完成，不怕杀进程）
  run(&apos;systemctl restart api-bridge&apos;, REPO_DIR);
}
```

注意几个细节：
- **`BASE_URL=/`**：systemd 启动的服务没有这个环境变量，必须显式传入，否则 Astro 不知道 `base` 该用啥
- **步骤顺序**：API Bridge 依赖先装（不重启），博客构建部署完成后最后才 `restart`，避免中途杀进程
- **不用 `cp`**：因为服务器上 `git clone` 的仓库就在 `/var/www/kaitohub`，Nginx 的 `root` 直接指向它的 `dist/` 子目录，构建产物天然就在正确位置，不需要额外复制

### 2. CI 端：一个 curl 搞定

GitHub Actions 的部署步骤从 40 行 SSH 命令简化为 8 行：

```yaml
- name: Trigger Alibaba Cloud deploy
  env:
    HOST: ${{ secrets.ALIYUN_HOST }}
    DEPLOY_SECRET: ${{ secrets.DEPLOY_SECRET }}
  run: |
    curl -X POST &quot;http://$HOST/api/deploy&quot; \
      -H &apos;Content-Type: application/json&apos; \
      -d &quot;{\&quot;secret\&quot;:\&quot;$DEPLOY_SECRET\&quot;}&quot;
```

只需要两个 Secret（`ALIYUN_HOST` + `DEPLOY_SECRET`），不再需要 SSH 私钥。

### 3. Nginx 代理

API Bridge 监听 `127.0.0.1:3001`，Nginx 把 `/api/*` 代理过去：

```nginx
location /api/ {
    proxy_pass http://127.0.0.1:3001;
}
```

`/api/deploy` 自然也在代理范围内，不需要额外开端口。

## 实战中踩的三个坑

### 坑一：Shiki 不认识 `vbnet`

旧博客里有一段代码标记为 `vbnet`，但实际是 Go 代码。之前在 GitHub Actions 上构建时 Shiki 只是警告，换到服务器本地构建后，新版 Shiki 对未知语言处理变严格，直接导致构建失败。

**教训**：代码块的语言标记要写对的，别指望工具一直包容你。

### 坑二：systemd 环境没有 `BASE_URL`

`astro.config.mjs` 里写了 `base: process.env.BASE_URL || &apos;/&apos;`，本意是本地开发默认 `/`，CI 构建时注入。但 systemd 启动的服务压根没有这个环境变量，构建出来的页面路径全乱了。

**教训**：别假设环境变量一定存在，关键变量在命令中显式传入最稳妥。

### 坑三：多余的 `cp` 操作

最开始从 SSH 方案迁移时，顺手保留了 `rm -rf` + `cp -r` 的步骤。结果因为构建目录和 Nginx 目录是同一个，`cp` 报 `same file` 错误。删掉后才发现——根本不需要复制，构建产物已经在正确位置了。

**教训**：换方案时不要把旧逻辑无脑搬过来，想清楚每一步是否还有必要。

## 对比

| | SSH 推送方案 | Webhook 方案 |
|---|---|---|
| CI 配置 | ~40 行 SSH + tar | ~8 行 curl |
| 需要 Secrets | HOST + SSH_KEY | HOST + DEPLOY_SECRET |
| 安全组 | 需开放 22 端口 | 复用 80/443 |
| IP 白名单 | 5000+ 条不可行 | 不需要 |
| SSH 密钥维护 | 生成/分发/轮换 | 不需要 |
| 部署逻辑 | 写在 CI 里 | 服务器端控制 |
| 故障排查 | CI 日志有限 | journalctl -u api-bridge -f |

## 总结

这个方案的思路转变很简单：**从&quot;推&quot;变成&quot;拉&quot;**。

CI 不再负责把文件传到服务器，只发一个信号。服务器收到信号后自己 `git pull`、自己构建、自己部署。少了 SSH 这一层，也少了一大堆麻烦。

而且这个端点本身就架在 API Bridge 上，和 AI 聊天共用同一套基础设施——Nginx 代理、systemd 管理、限流保护。没有任何额外组件。

如果你的项目也在用 GitHub Actions 部署到自有服务器，不妨试试这个思路。只要服务器能访问 GitHub（能 clone 代码），就不需要 SSH。</content:encoded></item></channel></rss>