记录 | 大学里的最后一段时光 #日记楼
Vanitas Vanitatum Et Omnia Vanitas.
虚空的虚空,一切尽为虚空。《般若波罗蜜多心经》亦有言:“观自在菩萨,行深般若波罗蜜多时,照见五蕴皆空,度一切苦厄”。
古今中外,英雄豪杰无不如过江之鲫,于兴盛之时,凭天时地利人和,独领风骚,傲立潮头。殊不知千年之后,虽有“樯橹灰飞烟灭”之气魄,亦不免消失于滚滚历史洪流。
泛泛之辈如我,唯有专注于当下,珍惜自我所有之物,珍惜身边亲近的人,默默地走完本科的最后一段旅程。2025 年 06 月至 2026 年 06 月,我愿在此处不定期地记录下属于自己的一切。
一切皆空,唯有当下愈加贵重。万事喧嚣,愿有一方清静独处之处。愿此处小楼能成为承载我接下来一年的喜怒哀乐,愿自己能够在互联网的一角,留下属于自己的一点点痕迹。
开楼就用我最喜欢的一部动画《可塑性记忆 (プラスチックメモリス)》中的题词送给大家,也送给自己:

提示:可能有时候 lz 会有很负面的情绪,或者不恰当的表述,先向大家表示抱歉,如果楼主说了什么不太好的话或者令大家伤心的话,提示一下 lz 或者屏蔽楼主就好了,先向大家表示歉意,对不起,谢谢大家 🥰
楼里会有什么?
- 可愛いアイラちゃん
- 各种可能的动漫截图
- 每天的生活
- 风景图?周边地图?
- 🐷 😋 🤡
- lz 的焦虑和不安 (很抱歉,有时候控制不了自己 🥲)
- 可能的焦虑传播 (对不起,请一定要提醒 lz 删除(私信发帖都可以))
- 可能的压力和不安
cy ![]()
是新日记楼!
祝 lz 生活顺利🥹
长恨此身非我有 何时忘却营营
开篇先给出自己的假期计划吧!
古人有云:“取法乎上,仅得其中。取法乎中,仅得其下。”所以目标要兼具挑战性和可行性。
暑假的三个月打算:
上午 7:00 左右起床
上午:leetcode,复习数据结构,与其他 408 科目,顺序打算为数据结构 -> 计算机组成 -> 操作系统 -> 计算机网络。这些科目アイラちゃん在期末都是 90 分左右,因此希望会顺利一些 QAQ,为考研/推免都做好两手准备 🫠
中午/下午:做实验室实习的项目/阅读需要阅读的论文,或者在没有活的时候做一做 15445/6.824,学习 C++/Go。毕竟有希望就要去争取!
晚上:继续复习高数/线代。等到线性代数复习结束后再开概率论和数理统计。并且最后复习一些单词(不过英语应该不需要太担心,アイラちゃん很努力地通过了 Toefl, R25L25S25W26!)
希望自己能完成这个暑假的计划,fight on!!!
cy ![]()
アイラちゃん(指插图中的艾拉)今日もちょ可愛いです 🥰

🥰
アイラちゃん 🥰 🥰
艾拉可爱捏
如果能让アイラちゃん做我的彼女我就算住大别野也愿意口牙 ![]()
2025 年 5 月 30 日 星期五 天气:晴/多云
上午刷 leetcode,复习了一下 dijkstra 算法,正好今天的每日一题也是 dijkstra 算法~
C++
class Solution {
int n;
public:
void bfs(const vector<vector<int>> & graph, priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> & pq, vector<int> & distance, vector<int> & visited, const int & node) {
visited = vector<int>(n, 0);
pq.emplace(0, node);
visited[node] = 1;
distance[node] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
for(const auto & adj: graph[cur]) {
if(distance[adj] > distance[cur] + 1) {
distance[adj] = distance[cur] + 1;
pq.emplace(distance[adj], adj);
}
}
}
}
int closestMeetingNode(vector<int>& edges, int node1, int node2) {
n = edges.size();
vector<vector<int>> graph(n);
for(int i = 0; i < n; i++) {
if(edges[i] == -1) continue;
graph[i].emplace_back(edges[i]);
}
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
vector<int> distance1(n, INT_MAX / 2);
vector<int> distance2(n, INT_MAX / 2);
vector<int> visited;
bfs(graph, pq, distance1, visited, node1);
bfs(graph, pq, distance2, visited, node2);
// Get the distance. Then check the answer.
int ans_dist = INT_MAX / 2;
int ans = -1;
for(int i = 0; i < n; i++) {
int tmp_ans = max(distance1[i], distance2[i]);
if(ans_dist > tmp_ans) {
ans_dist = tmp_ans;
ans = i;
}
else if(ans_dist == tmp_ans) {
ans = min(ans, i);
}
}
return ans == INT_MAX / 2 ? -1 : ans;
}
};
C++
class Graph {
vector<vector<pair<int,int>>>graph;
vector<int> visited;
vector<int> distance;
int n;
public:
Graph(int n, vector<vector<int>>& edges) {
graph = vector<vector<pair<int,int>>>(n);
for(const auto & edge: edges) {
int from = edge[0], to = edge[1], edgeCost = edge[2];
graph[from].emplace_back(to, edgeCost);
}
Graph::n = n;
}
void addEdge(vector<int> edge) {
int from = edge[0], to = edge[1], edgeCost = edge[2];
graph[from].emplace_back(to, edgeCost);
}
int shortestPath(int node1, int node2) {
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
visited = vector<int>(n, 0);
distance = vector<int>(n, INT_MAX / 2);
pq.emplace(0, node1);
distance[node1] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(visited[cur]) continue;
visited[cur] = 1;
for(const auto & adj: graph[cur]) {
const auto [adj_to, adj_w] = adj;
if(distance[adj_to] > distance[cur] + adj_w) {
distance[adj_to] = distance[cur] + adj_w;
pq.emplace(distance[adj_to], adj_to);
}
}
}
return distance[node2] == INT_MAX / 2 ? -1 : distance[node2];
}
};
/**
* Your Graph object will be instantiated and called as such:
* Graph* obj = new Graph(n, edges);
* obj->addEdge(edge);
* int param_2 = obj->shortestPath(node1,node2);
*/
C++
constexpr int directions[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
class Solution {
public:
int minTimeToReach(vector<vector<int>>& moveTime) {
// [dist, x, y];
priority_queue<tuple<int,int,int>, vector<tuple<int,int,int>>, greater<tuple<int,int,int>>> pq;
pq.emplace(0, 0, 0);
int rows = moveTime.size(), cols = moveTime[0].size();
vector<vector<int>> distance(rows, vector<int>(cols, INT_MAX / 2));
vector<vector<int>> visited(rows, vector<int>(cols, 0));
distance[0][0] = 0;
while(!pq.empty()) {
const auto [dist, x, y] = pq.top();
pq.pop();
if(visited[x][y]) continue;
visited[x][y] = 1;
for(int i = 0; i < 4; i++) {
int nx = x + directions[i][0], ny = y + directions[i][1];
if(nx >= 0 && nx < rows && ny >= 0 && ny < cols) {
int temp_distance = max(dist, moveTime[nx][ny]) + (x+y) % 2 + 1;
if(temp_distance < distance[nx][ny]) {
distance[nx][ny] = temp_distance;
pq.emplace(temp_distance, nx, ny);
}
}
}
}
return distance[rows - 1][cols - 1];
}
};
跟着 灵神的题单 走感觉有进步的说!
然后折磨アイラちゃん四天只睡了 16 个小时的 PPG 算法也算是成功实现了,并且确实收敛的速度更快,只是为什么稳定性不好呢 QAQ
下午上数据库原理课,听不懂事务系统 🐷 时间戳控制事务法和 2PL 法更是稀里糊涂 🫠 后面暑假再仔细看看吧~反正不考
晚上学了一下 Go,这门语言还是很有属于自己的风范的,感觉有集百家之长的特性,希望我能稍微掌握一下吧~(这也是兴趣的一部份呢)
最后,提前祝大家晚安好梦的说! 🥰 也祝大家期末顺利!
口瓜怎么日记帖被拿下了 🥲 5 月 30 日的日记贴变成垃圾消息被 Akismet ちゃん拿下了 🥺
是不是主包发 leetcode 题目和代码重复性太高被拿下了 🫠

アイラちゃん 我们土塬貌似是这样的🤔不时会出现这种吞帖的情况
感觉可以向站务提出说明?
站务应该会看到的,就不再单独麻烦他们了 🥺
akismet ちゃん好严厉,有坏坏的大姐姐感觉 🤣
アイラちゃん 豪德 🫠
好饿,出去觅食了 🐖
好耶,帖子回来啦 🥰
谢谢站务 😘
有 C/C++ 基础看起一门新的语言还是很快的,只是迫切地需要熟练度
从例子入手后再去仔细地阅读语法,希望这样能够快速入门 Go, 然后尝试 6.824
p.s.现在变成 6.5840 了,待我学会 go+ 有时间后尝试一下 😋
每日一题 还是可以 Dijkstra 的,毕竟 Dijkstra 就是 BFS 中的队列->优化队列而已~
坐标变换让我成为 🐖
C++-1 (Dijkstra)
class Solution {
public:
int n;
int CheckTransfer(int nums, vector<vector<int>> & board) {
int row, col;
if(nums % n == 0) {
row = n - nums / n;
if(row % 2 != n % 2) {
col = n-1;
}
else {
col = 0;
}
} else {
row = n - 1 - nums / n;
if(row % 2 != n % 2) {
col = (nums % n) - 1;
}
else {
col = n - (nums % n);
}
}
// // ** debug **
// cout << nums << " at: (" << row << " ," << col << ")\n";
return board[row][col];
}
int snakesAndLadders(vector<vector<int>>& board) {
// Build Matrix.
n = board.size();
vector<int> distance(n*n+1, INT_MAX / 2);
vector<int> visited(n*n+1, 0);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
pq.emplace(0,1);
distance[1] = 0;
while(!pq.empty()) {
// BFS to get the adjacent point.
const auto [dist, cur] = pq.top();
pq.pop();
if(visited[cur]) continue;
visited[cur] = 1;
for(int i = cur + 1; i <= min(cur + 6, n * n); i++) {
int transfer = CheckTransfer(i, board);
int adj = transfer == -1 ? i : transfer;
if(distance[adj] > distance[cur] + 1) {
distance[adj] = distance[cur] + 1;
pq.emplace(distance[adj], adj);
}
}
}
return distance[n * n] == INT_MAX / 2 ? -1 : distance[n * n];
}
};
C++-2 (BFS)
class Solution {
public:
int n;
int CheckTransfer(int nums, vector<vector<int>> & board) {
int row, col;
if(nums % n == 0) {
row = n - nums / n;
if(row % 2 != n % 2) {
col = n-1;
}
else {
col = 0;
}
} else {
row = n - 1 - nums / n;
if(row % 2 != n % 2) {
col = (nums % n) - 1;
}
else {
col = n - (nums % n);
}
}
// // ** debug **
// cout << nums << " at: (" << row << " ," << col << ")\n";
return board[row][col];
}
int snakesAndLadders(vector<vector<int>>& board) {
// Build Matrix.
n = board.size();
vector<int> visited(n*n+1, 0);
queue<pair<int,int>> q;
q.emplace(1, 0);
while(!q.empty()) {
// BFS to get the adjacent point.
const auto [cur, dist] = q.front();
q.pop();
visited[cur] = 1;
for(int i = cur + 1; i <= min(cur + 6, n * n); i++) {
int transfer = CheckTransfer(i, board);
int adj = transfer == -1 ? i : transfer;
if(visited[adj]) continue;
if(adj == n * n) return dist + 1;
visited[adj] = 1;
q.emplace(adj, dist + 1);
}
}
return -1;
}
};
成就 badges

不知不觉三个月了,希望有点进步的说
被 xiong 器裹挟的アイラちゃん

今天是端午节,祝塬友们端午快乐!
也祝 @萨卡兹雇佣兵 生日快乐 🥰
アイラちゃん 谢谢泥喵
(话说我也谢了泥好多次了 🤪)
惊觉一门课下周四有随堂考还有 40% 的分数
天塌了,我说怎么这门课没有大作业,原来是今天跟我说改成随堂考了
好好好 🥺
发挥优良传统,一支笔,一个 graffiti,一个晚上,一个奇迹 🐖
アイラちゃん 集百家之长的特性
好乐,难得见一个不骂的
2025 年 5 月 31 日
今天是五月的最后一天!端午假期快乐!
惊觉一门课下周四有随堂考还有 40% 的分数
天塌了,我说怎么这门课没有大作业,原来是今天跟我说改成随堂考了
前情提要⬆️
于是战复习,复习模型压缩,剪枝,量化策略,复习 TPU 脉动阵列模型,数据流不变方式复用比,amdahl 定理,并行加速比 😴
不过已经复习完一半了!明天就可以复习结束的说!
明天可能要下大暴雨了,真讨厌下雨啊,每次鞋都会湿完,但是还是得来图书馆的,在宿舍只想睡觉就好了 🐖
骑士团和狐狸小队的麻麻们

最后,祝大家晚安好梦~
口瓜,已经下雨了,怎么回事口牙
アイラちゃん 上海怎么天天下雨 ![]()
神秘小维 江淮气旋 + 梅雨=暴雨 🥲

明天的雨势更加猛烈 😵💫

2929. 给小朋友们分糖果 II (1701M)(2025.06.01)
数学题~ 祝大家儿童节快乐 ![]()
Go
func distributeCandies(n int, limit int) int64 {
var ans int64
ans = 0
for i := 0; i <= min(limit, n); i++ {
ans = ans + int64(max(int64(min(limit, n-i)) - int64(max(n-i-limit, 0))+1, 0))
}
return ans
}
C++
class Solution {
public:
long long distributeCandies(int n, int limit) {
long long ans = 0;
for(int i = 0; i <= min(limit, n); i++) {
ans += (long long)max(min(limit, n-i) - max(n-i-limit, 0) + 1, 0);
}
return ans;
}
};
神秘小维 正在宾馆里躲雨 ![]()
此人不语,只是一味地在门里生产废料。
上午复习完了周四的考试,实现了昨天在 神秘小维的赛博小窝 的誓言 ![]()
借你吉言,明天一定一定认真复习 🫡
战论文,但是感觉看的很慢而且很难抓住要点
很想了解其他人是怎么快速看完论文,抓住要点并且学习的。一般一篇论文アイラちゃん总会花上 3-5 天(全力)才能看完......
下午看的论文是 sigcomm'14 很经典的 CONGA,一篇针对数据中心的分布式拥塞感知与负载均衡方案。(只看完了动机和目的。。。 🐷)
部分的要点
CONGA 观察到了 ECMP 在负载均衡方面的缺陷:
- 爆发大流时受限于哈希随机分配,碰撞概率增大导致负载不均衡。
- 仅关注局部信息而未关注到潜在下游通路的拥塞影响。
导致了链路故障情况下即使有冗余也使得成功送达流量下降 40%。
并且,针对其他解决方案的不足:如中心化调度导致的数据中心场景下流量快速变化的反应不足,局部路由特性对全局掌握不佳,基于终端主机的传输层协议增加传输层复杂度并且无法控制绕开内核具有自身协议栈的高性能应用,提出了属于自己的方案:
- 分布式框架,允许在 RTT 尺度下进行拥塞反应。
- 全局拥塞信息控制,leaf-leaf 控制快速传输网络拥塞指标
- 独立于传输层的网络层部署,避免影响终端主机和引入复杂的传输层控制。
需要满足:高效性,传输层独立,不对称性稳定,可增量部署,可以针对 Leaf-spine 拓扑优化。

晚上要复习数学了~论文明天再继续看,CONGA 是怎么设计的。
图来自 X: @ sei_umehara

アイラちゃん 看不懂喵😭
窝是来看美图的喵 🫠
尽力做到每大更一次贴一张图 🥰
アイラちゃん 好的喵 🥰
2025 年 6 月 1 日 星期一 天气:中雨
上午:
https://xjtu.app/t/topic/14653/23
下午:
https://xjtu.app/t/topic/14653/25
晚上:
先结束了一元积分的基础复习。先把知识点和基础题都过一遍就好。
数学复习到了多元函数微分,最为头疼的还是多元函数连续/可导/可微/存在极限的关系,还是要记一下这张图 😵💫

祝各位晚安,好梦~ 🥰
X: @Tenkaichiho0

アイラちゃん lz 好自律🥹
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 大一大二一点不自律 🥲现在只是为了抱佛脚,看看能不能继续上学 😭
其实我更想打 MC,每天 🐖,但是边缘人太折磨了,又不想太早出来工作,想读硕士后再工作
アイラちゃん 话说 LZ 有 paper 的 readlist 吗,还是组里相关的任务什么的
Zerick 是暑期打算在实验室实践(必修,可以走企业实践也可以在学校实验室实践),偏网络仿真方向(开发),目前是学长给了“负载均衡”的相关文献调研,所以读论文,后面还要上 ns3 开发 😇
アイラちゃん 每天 🐖
我也想做一只快乐的皮革兔🥹
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 其实有好多想做的事
想自驾游从上海回家(海南海口),想学日语考过 N2 然后出国玩一下,想要自驾游从四川到西藏,有很多想做的事,如果有机会一定会好好玩一玩 😋
高考结束后的那个暑假几乎完全没有了,后面每个暑假都被小学期所占据,没有多少时间玩,有点可惜 🥲
悟已往之不谏,知来者之可追
アイラちゃん 很想了解其他人是怎么快速看完论文,抓住要点并且学习的。
+1,我现在非常容易陷入到实现里出不来,但是不看实现又 get 不到 paper 在干什么。
感觉还是读得太少了不知道学界现状,缺少 taste(x
Zerick 对的,可能是需要积累吧。现在总是有很多陌生的术语/词汇,所以阅读的时候磕磕绊绊,但是读多了积累多了应该就快一些了 ![]()
アイラちゃん 想自驾游从上海回家(海南海口),想学日语考过 N2 然后出国玩一下,想要自驾游从四川到西藏
好厉害啊🥹
希望 lz 有朝一日能实现这些目标吧
长恨此身非我有 何时忘却营营
アイラちゃん 同 😭驻波感觉目前不靠任何辅助阅读的手段就会读的很慢,或者晕乎乎的💦常常是看到一半就忍不住让 GPT 翻译了 + 总结要点
アイラちゃん在 zotero 里加装了 bionic 插件
https://github.com/windingwind/bionic-for-zotero

效果是这样的,会稍微有所提升阅读速度,浏览器アイラちゃん也会在阅读长 blog 的时候启用插件辅助阅读
一般来说长文阅读アイラちゃん很容易丢失上下文信息或者不专注(包括中文),阅读很慢,这样会稍微快一些
アイラちゃん 这样吗,有点意思🤔
(确实感觉怪提神的 ![]()
驻波回去也试试喵
今天的天气很舒服,所以上午偷懒了一个小时才起床 🥲
X:@Gameillust_AI

上午结束!说实话有些遗憾,因为刷 leetcode 解题的速度还是比较慢,需要进一步熟悉知识点 + 提升解题/写代码的速度。
每日一题是经典的贪心!
135. 分发糖果 (H)(2025.06.02)
C++
class Solution {
public:
int candy(vector<int>& ratings) {
// Two times iteration.
vector<int> ans;
int count = 1;
ans.emplace_back(count++);
for(int i = 1; i < ratings.size(); i++) {
if(ratings[i] > ratings[i-1]) {
ans.emplace_back(count++);
}
else {
count = 1;
ans.emplace_back(count++);
}
}
#ifdef DEBUG
cout << ans.size() << ' ' << ratings.size() << '\n';
for(int i = 0; i < ans.size(); i++) {
cout << ans[i] << ' ';
}
cout << '\n';
#endif
count = 1;
for(int i = ratings.size() - 1; i > 0; i--) {
if(ratings[i-1] > ratings[i]) {
ans[i-1] = max(ans[i-1], ++count);
}
else {
count = 1;
ans[i-1] = max(ans[i-1], count);
}
}
#ifdef DEBUG
cout << ans.size() << ' ' << ratings.size() << '\n';
for(int i = 0; i < ans.size(); i++) {
cout << ans[i] << ' ';
}
cout << '\n';
#endif
return reduce(ans.begin(), ans.end());
}
};
Go
func candy(ratings []int) int {
n := int(len(ratings))
ans := make([]int, n)
count := 1
ans[0] = count
count++
for i := 1; i < n; i++ {
if ratings[i-1] < ratings[i] {
ans[i] = count
count++
} else {
count = 1
ans[i] = count
count++
}
}
count = 1
for i := n - 1; i > 0; i-- {
if ratings[i-1] > ratings[i] {
count++
ans[i-1] = max(ans[i-1], count)
} else {
count = 1
}
}
var final int = 0
for i := 0; i < n; i++ {
final = final + ans[i]
}
return final
}
继续复习图论:这是图论中经典的拓扑排序,采用 Kahn 算法。
https://oiwiki.com/graph/topo/#kahn-算法
C++
好麻烦的题目
class Solution {
public:
bool isPrintable(vector<vector<int>>& targetGrid) {
// The tuple has color -> (up, down, left, right).
unordered_map<int, tuple<int,int,int,int>> color_mat;
// Find the tuple mat from the graph.
for(int i = 0; i < targetGrid.size(); i++) {
for(int j = 0; j < targetGrid[i].size(); j++) {
int color = targetGrid[i][j];
if(color_mat.find(color) == color_mat.end()) {
color_mat[color] = make_tuple(INT_MAX / 2, 0, INT_MAX / 2, 0);
}
const auto & [up, down, left, right] = color_mat[color];
color_mat[color] = make_tuple(min(up, i), max(down, i), min(left, j), max(right, j));
}
}
// Search the matrix and find the topology order.
unordered_map<int, unordered_set<int>> graph;
unordered_map<int, int> inDegree;
for(const auto & ele: color_mat) {
const int & color = ele.first;
const auto & [up, down, left, right] = ele.second;
graph[color] = unordered_set<int>();
if(inDegree.find(color) == inDegree.end()) {
inDegree[color] = 0;
}
for(int i = up; i <= down; i++) {
for(int j = left; j <= right; j++) {
if(targetGrid[i][j] != color && graph[color].find(targetGrid[i][j]) == graph[color].end()) {
graph[color].insert(targetGrid[i][j]);
inDegree[targetGrid[i][j]]++;
}
}
}
}
int n = graph.size();
queue<int> q;
int count = 0;
for(const auto & ele: inDegree) {
if(ele.second == 0) {
q.emplace(ele.first);
}
}
while(!q.empty()) {
int cur = q.front();
q.pop();
count++;
for(const auto & adj: graph[cur]) {
if(--inDegree[adj] == 0) {
q.emplace(adj);
}
}
}
#ifdef DEBUG
for(const auto & element: color_mat) {
const auto & [u,d,l,r] = color_mat[element.first];
cout << element.first << ":(" << u << ',' << d << ',' << l << ',' << r << ")\n";
}
for(const auto & element: graph) {
cout << element.first << " -> (";
for(const auto & adj: element.second) {
cout << adj << ',';
}
cout << '\n';
}
for(const auto & element: inDegree) {
cout << element.first << ": " << element.second << '\n';
}
cout << count << ' ' << n << '\n';
#endif
return count == n;
}
};
下午要和同学做星期四上午的 pre,以及交换星期二的数据挖掘推荐系统的展示 demo。看来今天下午又无法读论文了......没有意义的大作业真的害人不浅,强烈抨击金课! 😤 先去 🐷啦~
🐖

アイラちゃん 自律 🏃
i-BuProfen 再不跑就生锈了 🥲
现在又下雨了完蛋了 😇
アイラちゃん 好自律
对我来说 跑步一直是极度痛苦的事情😖
长恨此身非我有 何时忘却营营
アイラちゃん 既不想上学也不想上班 我也想每天 🐖
想到将来还要至少上 20 年班我就不行了😭
此人不语,只是一味地在门里生产废料。
post deleted by author 我也不想当社畜 😭
post deleted by author 为什么才 20 年
鳳 笑夢 三十五岁危机 ![]()
此人不语,只是一味地在门里生产废料。
2025 年 6 月 2 日 星期一 天气:小雨转阴
上午:
https://xjtu.app/t/topic/14653/43
下午和晚上:和同学完成星期四 ppt 的制作,以及星期二 demo 的制作。但是没想到我的 baseline 数据竟然不见了!平台的监控数据只保存 14 天,所以今晚要赶紧取得数据补充好我们的 PPT。PPT 上涉及到了模型、算法改动后与 baseline 的对比,虽然效果好像都没有比 baseline 好,甚至比 baseline 差,但还是要展示出来,分析出来效果不好的原因。但是同学训练的 reward 版本很好。
晚上一直在处理数据和 PPT,一点也不想搞了,好累口牙~~~
https://xjtu.app/t/topic/14653/44
希望能坚持自律~不然身体会垮掉的 😥
明天又要上课了,不想上课,不想工作 🥲 只想快乐地 🐷~
祝大家有美好的新的一周!晚安~
X: @ComikeMaster

坚持每日一题是我的 leetcode 账户名,所以就算今天上午 pre,我也要完成它!
(小声:其实做过了)
1298. 你能从盒子里获得的最大糖果数 (1825H)(2025.06.03)
C++
class Solution {
public:
int maxCandies(vector<int>& status, vector<int>& candies, vector<vector<int>>& keys, vector<vector<int>>& containedBoxes, vector<int>& initialBoxes) {
// 2 times bfs. First get all the boxes final status. Second get the candies.
queue<int> q;
for(const auto & init: initialBoxes) {
q.emplace(init);
}
while(!q.empty()) {
int cur = q.front();
q.pop();
if(status[cur] == 1) {
for(const auto & key: keys[cur]) {
status[key] = 1;
}
}
for(const auto & adj: containedBoxes[cur]) {
q.emplace(adj);
}
}
// Then get the boxes candies.
int ans = 0;
for(const auto & init: initialBoxes) {
q.emplace(init);
}
while(!q.empty()) {
int cur = q.front();
q.pop();
if(status[cur] == 1) {
ans += candies[cur];
}
for(const auto & adj: containedBoxes[cur]) {
q.emplace(adj);
}
}
return ans;
}
};
下午先战数据库作业,然后准备后天 pre 的材料 + 再看看做好的复习笔记准备后天下午的考试!
祝自己一切顺利!
X: @kasuga_iz

感觉好像什么都在坏掉
今天感觉心脏有点难受,一个月前买的 pad 的充电线坏了,Mac 的充电线也坏了,type-C aux 转接线也坏了,跑步的鞋也开裂了,今早书包也坏了一个口子出来(看错了,书包是好的 🤣)
只能重新买了吗,但是开销挺大的 🥲
アイラちゃん 那很坏了 🤣
🫳
战数据库事务处理 😤
晚上再补齐 OCC( ✅️ )和 MVCC ⭕️ 🐷
X:@haimiya_neno

事务处理与并发控制
事务基础与并发控制
事务
- 事务 (Transaction) 是什么?
- 是一组数据库运算的集合,可以抽象成一个的不可分的逻辑工作单元
ACID 特性
- 事务具有 ACID 特性
- Atomicity: 原子性,即不可分割。事务要么完全执行,要么完全不执行。
- Consistency: 事务必须保持数据库系统满足持续性,也就是满足数据库系统中的原先拥有的关系和条件。
- Isolation: 隔离性,每个事务都是独立运行,相当于仅仅在单独运行自己。
- Durability: 持续性。事务必须可以从错误中恢复。
并发控制
- 什么是并发控制?并发控制的目的是什么?
- 并发控制是数据库系统保证多个事务运算正确交叠的机制。目的是为了保证事务之间的隔离性,同时增加数据库处理速度,提高吞吐率。
保证正确的交叠:从序列化执行开始
为了简化调度方式,将数据库内的所有操作都看做仅有对特定资源的读取和写入操作。我们用 表示第 i 个事务读取数据 A, 表示第 i 个事务写入数据 A。
序列化调度与等价调度
- 没有任何交叠操作的调度 -> 序列化调度。即完成一个事务的所有操作才去执行下一个事务的操作。
- 等价调度是一组执行效果相同的调度的集合。
可以序列化的调度
- 具有交叠操作的调度等价于某些事务的序列化操作。
- 每个可序列化的调度都可以确保持续性的时候,我们的序列化调度是可持续的。
- 需要我们判断调度是否可序列化。
判断调度可序列化
矛盾操作
- 一对矛盾的操作将具有:
- 不同的事务面向同一份资源。
- 至少有一个操作是写操作。
- 这样我们有:(R1W2 不可重复读),(W1R2 脏页读),(W1W2 更新丢失) 均为矛盾操作。(目前不考虑假性读取和写入偏移)
可序列化的不同层级
-
冲突可序列 (confilict serializability)
- 两个调度是冲突等价的,当且仅当
- 拥有相同事务且事物内部操作相同。
- 两个调度中冲突操作的顺序是相同的。也就是说如果调度一中是 (R1W2),那么调度二也必须保证 R1 在 W2 之前。
- 调度 是冲突可序列的,当它
- 在冲突操作上的顺序与某些序列化调度相同。也就是说,你可以将他转换成序列化调度。
- 两个调度是冲突等价的,当且仅当
-
视图可序列 (view serializability)
- 两个调度是视图等价的当它们
- 事务 1 在调度 1 中读取的初始值和在调度 2 中读取的初始值相同。
- 事务 1 在调度 1 中读取到事务 i 写入的值,则在调度 2 中也应该读取到事务 i 写入的值。
- 所有的最终结果都必须由一个事务来完成。
- 两个调度是视图等价的当它们
-
很显然冲突可序列化是比视图可序列化更严格 的。
前序图
- 判断是否冲突可序列化的简单的方法。
- 每个事务作为一个节点。每个冲突操作对作为一条边。
- 当前序图是有向无环图时,恭喜你它是冲突可序列化的。
并发控制方法
为了保证得到正确的交叠操作调度,我们需要一些并发控制方法。
锁的类型(其实是读写锁换皮)
- 共享锁:用于读操作的共享锁。
- 排它锁:用于写操作的排它锁。
| 可执行判断 | 共享锁 | 排它锁 |
|---|---|---|
| 共享锁 | ✅️ | ❌️ |
| 排它锁 | ❌️ | ❌️ |
简单的排它锁和共享锁无法保证可序列性。
二阶段锁
-
阶段一:获得锁
- 每个事务获取他所需要的锁。
- 可能被拒绝或允许,但是在该阶段不允许有释放锁的行为。
-
阶段二:释放锁
- 每个事务释放它所需要的锁。
- 不允许任何事务获得锁。
- 严格的二阶段锁:在所有事务结束后才能统一释放锁。可以解决脏页读问题。
- 二阶段锁可能导致死锁。通过检测和预防来避免死锁 (银行家算法等...)
- 这里不探索如何解决死锁。
-
一个调度是严格的当一个事务写入一份数据后,其他事务不能读取或覆盖这份数据,直到该事务终结。
-
可以看出严格程度:严格序列化 > 严格二阶段锁 > 冲突可序列化 > 视图可序列化
时间戳排序控制 (T/O)
- 采用时间戳来决定序列化调度中事务执行的顺序。当 TS(T1)<TS(T2) 的时候,执行顺序必须为 T1 -> T2.
- 基础时间戳
- 事务自身将拥有时间戳;TS( )
- 每个数据对象也将有自己的时间戳。
- 数据 X 的读时间戳:R(X)
- 数据 X 的写时间戳:W(X)
基础时间戳控制
-
读操作
- 当 TS(T) < W(X) 的时候,回滚并重新开始,赋予新的 TS(T).
- 否则持续执行并且 .
- 允许拷贝一份 X 来保证重复读取。
-
写操作
- 当 TS(T) < W(X) 或者 TS(T) < R(X) 的时候,回滚并重新开始,赋予新的 TS(T).
- 否则持续执行并且
-
基础的时间戳操作保证了冲突可序列性质。
托马斯写 + 时间戳控制
-
写操作处有所不同。
- 当 TS(T) < R(X) 的时候,回滚。
- 当 TS(T) < W(X) 的时候,忽略写操作,继续执行(因为很快时间戳后面的事务的写操作将覆盖本次的写操作)。
- 否则持续执行并且
-
很明显仅保证视图可序列性。
OCC(乐观并发控制)
- 时间戳协议。
- DBMS 为每一个事务创建了一个私有的工作空间。
- 每个被读取的对象将被复制到工作空间中,对对象的编辑也将加入到工作空间中。
- 当事务提交时,DBMS 将会对比工作空间写集合,每个事务的写空间相同时,装载到全局数据库中。
三阶段
- Read,读取。跟踪每个事务的读、写集合,储存他们的写入对象到私有的工作空间中。DBMS 将会复制一个事务中所有需要的元组到共享数据库中保证可重复读。
- Validation,验证。给每一个事务独有的时间戳,检查他们是否于其他对象相互冲突。
- 当事务唤起提交时,进入验证部分。
- 前向验证:检查正在提交中的事务读/写集合是否与活动中的未提交事务相互重叠。
- 检查时间戳。当提交事务时间戳小于其他活动事务时,需要满足三个条件:
- (1) 其他事务读阶段位于当前事务写阶段完成之后。R2(A)>W1(A)
- (2) 其他事务写阶段在当前事务写阶段完成之后。W2(A)>W1(A), 并且 当前事务没有修改任何其他活动事务要读取的对象。
- (3) 其他事务读阶段在当前事务读阶段完成之后。R2(A)>R1(A), 并且当前事务不修改任何T2 将会读取或写入的对象。
- 反向验证:检查正在提交中的事务读/写集合是否与活动中的已提交事务相互重叠。仍然遵循三条原则。
- 检查时间戳。当提交事务时间戳小于其他活动事务时,需要满足三个条件:
- Write,写。当验证成功时,对所有已经编辑过的对象设置写时间戳,装载他们进入全局数据空间。否则回滚,中断事务。
2025 年 6 月 3 日 星期二 天气:多云
上午:
Demo,以及
https://xjtu.app/t/topic/14653/53
下午和晚上:
https://xjtu.app/t/topic/14653/56
不过没太看明白 MVCC,明天补上来 Note,再 copy 到 blog 上~
星期五又要组会,要炸了,アイラちゃん还没看完 reading list,明天一整天要加油看完 🥲
祝大家好梦~晚安 nya
X:@OuToTmeM

忙晕了,提前更一下日记楼~
2025 年 6 月 4 日 星期三 天气:多云
上午修改明天准备汇报的 ppt,评测结果出来了,强化学习对抗实在是太折磨人了,Level3 的胜率才 25%,不过 Level2 和 Level1 我们的胜率都是 100%。还是可以的吧。
其实做了很多的尝试,更换模型和算法,但是很显然这是不正确的。对抗性最重要的就是 reward 设计,前面的部分应该是在 reward 设计完了后再进行的工作。战略上的失误使得我们还是失去了一些优势。果然盲目的勤奋是没有办法掩盖战略上的懒惰的。这一点需要深思。
下午抽时间 leetcode 保持手感。
C++
class Solution {
public:
string lastSubstring(string s) {
int i = 0, j = 1, n = s.size();
while (j < n) {
int k = 0;
while (j + k < n && s[i + k] == s[j + k]) {
k++;
}
if (j + k < n && s[i + k] < s[j + k]) {
int t = i;
i = j;
j = max(j + 1, t + k + 1);
} else {
j = j + k + 1;
}
}
return s.substr(i, n - i);
}
string answerString(string word, int numFriends) {
if(numFriends == 1) {
return word;
}
string last = lastSubstring(word);
int n = word.size(), m = last.size();
return last.substr(0, min(m, n - numFriends + 1));
}
};
C++
#define DEBUG
#undef DEBUG
class Solution {
public:
string alienOrder(vector<string>& words) {
unordered_map<char, unordered_set<char>> graph;
unordered_map<char, int> inDegree;
bool flag = false;
auto add_edge = [&](const string & l, const string & r) -> void {
int i = 0;
for(i = 0; i < min(l.length(), r.length()); i++) {
if(graph.find(l[i]) == graph.end()) {
graph[l[i]] = unordered_set<char>();
}
if(inDegree.find(l[i]) == inDegree.end()) {
inDegree[l[i]] = 0;
}
if(l[i] != r[i] && graph[l[i]].find(r[i]) == graph[l[i]].end()) {
flag = true;
graph[l[i]].insert(r[i]);
inDegree[r[i]]++;
break;
}
else if(l[i] != r[i]) {
flag = true;
break;
}
}
if(!flag && l.length() > r.length()) {
return;
}
flag = true;
int temp = i;
for(; i < l.length(); i++) {
if(graph.find(l[i]) == graph.end()) {
graph[l[i]] = unordered_set<char>();
}
if(inDegree.find(l[i]) == inDegree.end()) {
inDegree[l[i]] = 0;
}
}
i = temp;
for(; i < r.length(); i++) {
if(graph.find(r[i]) == graph.end()) {
graph[r[i]] = unordered_set<char>();
}
if(inDegree.find(r[i]) == inDegree.end()) {
inDegree[r[i]] = 0;
}
}
};
// Step 1: Add edges.
if(words.size() == 1) {
flag = true;
for(int i = 0; i < words[0].length(); i++) {
if(graph.find(words[0][i]) == graph.end()) {
graph[words[0][i]] = unordered_set<char>();
}
if(inDegree.find(words[0][i]) == inDegree.end()) {
inDegree[words[0][i]] = 0;
}
}
}
else {
for(int i = 0; i < words.size(); i++) {
for(int j = i + 1; j < words.size(); j++) {
add_edge(words[i], words[j]);
}
}
}
#ifdef DEBUG
for(auto & elem: graph) {
cout << elem.first << ": ";
for(const auto & adj: elem.second) {
cout << adj << ' ';
}
cout << '\n';
}
for(auto & elem: inDegree) {
cout << elem.first << ": " << elem.second << "\n";
}
string flag_str = (flag == true) ? "true" : "false";
cout << flag_str << '\n';
#endif
// Step 2: TopoSort.
queue<char> q;
string ans;
for(const auto & ch: inDegree) {
if(ch.second == 0) {
q.emplace(ch.first);
}
}
while(!q.empty()) {
char ch = q.front();
ans.push_back(ch);
q.pop();
for(const auto & adj: graph[ch]) {
if(--inDegree[adj] == 0) {
q.emplace(adj);
}
}
}
return ans.length() == graph.size() && flag ? ans : "";
}
};
C++
基于基环树的思想。
class Solution {
public:
int maximumInvitations(vector<int>& favorite) {
int n = favorite.size();
vector<vector<int>> reverse_graph(n);
queue<int> q;
vector<int> inDegree(n, 0);
for(int i = 0; i < n; i++) {
inDegree[favorite[i]]++;
}
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) {
q.emplace(i);
}
}
while(!q.empty()) {
int cur = q.front();
q.pop();
// Build reverse_graph;
reverse_graph[favorite[cur]].emplace_back(cur);
if(--inDegree[favorite[cur]] == 0) {
q.emplace(favorite[cur]);
}
}
// Dfs reverse graph find the shortest edge.
auto dfs = [&](this auto && dfs, int from) -> int {
int max_depth = 1;
for(const auto & adj: reverse_graph[from]) {
max_depth = max(max_depth, dfs(adj) + 1);
}
return max_depth;
};
// Find the cycle.
int ring = 0, chain = 0;
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) {
continue;
}
inDegree[i] = 0;
int tmp_ring = 1;
for(int adj = favorite[i]; adj != i; adj = favorite[adj]) {
inDegree[adj] = 0; // mark as visited.
tmp_ring++;
}
if(tmp_ring == 2) {
chain = chain + dfs(i) + dfs(favorite[i]);
} else {
ring = max(ring, tmp_ring);
}
}
return max(chain, ring);
}
};
然后焦虑/抑郁状态出现了,突然一下子什么都不想干,只能停下手中的学习任务去外面走一走 🥲
拍照的地方在兰香湖附近,是アイラちゃん心情不好的时候最常去的地方。只要在安静的地方发上半个小时的呆,好像什么都会变好的。有时候止不住地想,是不是我选择了错误的道路,选择了错误的人生,还是什么时候做了错误的事情呢,以至于现在进退两难,也不知道未来在哪里,为了尝试保研也没有认真的去找工作。如果,如果考研和保研都失败了,没有实习经验的アイラちゃん又应该怎样才能找到理想的工作呢,如果不找本专业的工作我又会什么呢。现在的アイラちゃん还不知道答案是什么,未来在何处。



今天晚上就是准备明天下午的考试了,希望能够一切顺利吧,明天上午的 pre,明天下午的考试。星期五的组会明天考完试再准备吧。
祝大家今晚好梦~
X: @ji10me

アイラちゃん 现在的アイラちゃん还不知道答案是什么,未来在何处。
我也是😭
此人不语,只是一味地在门里生产废料。
アイラちゃん 如果,如果考研和保研都失败了,没有实习经验的アイラちゃん又应该怎样才能找到理想的工作呢,如果不找本专业的工作我又会什么呢。
答案是并不会怎么样 😁 你想太多了
IO 确实,这种东西太看时代和机遇了。坏时代诺奖得主也要变卖奖牌维持生计,国内院士扫大街;好时代没读过书也能有机会成为顶流网红,带货千万。
2025 年 6 月 5 日 星期四 天气:多云
心情很不好呀。
同一时期投简历的同学今天都收到了面试邀请然后进行了面试,但是我连面试通知都没有收到。(是学校统一安排的专业暑期实习)
虽然打算在学校内实验室进行实习,但是知道自己甚至连面试条件都不准入还是有些伤心的。可能自己的水平确实不够吧,自己平庸的成绩和平庸的表现也确实很难入别人的法眼。接下来还是应该默默地继续努力准备考研,在收留我的实验室组里做一些工作,多学习一些吧。
实力才是硬道理。没有成绩,没有项目傍身的我属实单薄无力。我的喜欢和热爱可能不会成为我最后选择的道路吧,有可能我会在未来放弃计算机,放弃 coding 吧,当真正没有办法的时候。
不过目前アイラちゃん不会那么轻易放弃的。至少让我丑陋地挣扎一下吧,为了一点微不足道的可能。
今天上午的题目是经典的并查集。只需要按照字典序进行 merge 即可。
C++
// #define debug
class Union {
public:
vector<int> parent;
int n;
// The Costruction function.
Union(int n):n(n),parent(vector<int>(n)) {
iota(parent.begin(), parent.end(), 0);
}
// Path compression. Find parent until the same.
int find(int id) {
if(parent[id] != id) {
parent[id] = find(parent[id]);
}
return parent[id];
}
// Merge by size. You can also make it by rank, for modified as size[pa_i] += 1;
void merge(int id1, char id2) {
// Find parent.
int pa1 = find(id1);
int pa2 = find(id2);
if(pa1 == pa2) {
return;
}
else if(pa1 < pa2) {
parent[pa2] = pa1;
}
else {
parent[pa1] = pa2;
}
}
bool judge(int id1, int id2) {
return find(id1) != find(id2);
}
};
class Solution {
public:
string smallestEquivalentString(string s1, string s2, string baseStr) {
Union u(26);
for(int i = 0; i < s1.length(); i++) {
u.merge(s1[i] - 'a', s2[i] - 'a');
}
#ifdef debug
for(auto & element: u.parent) {
cout << element.first << ' ' << element.second << '\n';
}
#endif
string ans;
for(int i = 0; i < baseStr.length(); i++) {
ans.push_back(u.find(baseStr[i] - 'a') + 'a');
}
return ans;
}
};
上午进行了 RL 的 pre,预计明天把报告干出来。
下午考完了试,也意味着期末周已经结束了。
晚上就是不停地赶报告,目前已经写完了更换模型部分 (MLP+LSTM) 更换成 transformer encoder-decoder,transformer+LSTM 部分,以及 dual PPO 和 PPG 的一部分。明天再把剩下的实验结果补充在报告中。只是有些可惜 SAC 的探索失败了,一个分布式框架我不太了解如何将数据序列化传输给训练主机,再将数据反序列化传递回 env,这就导致了我无法完成 off-policy 操作。
今天心情不太好,就不放图了,祝大家未来一帆风顺,今晚做个好梦,晚安。
アイラちゃん 好想大哭一场,但是已经很晚了,会影响到别人休息。大学三年我到底做到了什么呢?学会了什么呢?这三年我是尽力完成了,还是得过且过呢?如果是得过且过,为什么会如此疲惫不堪?如果不是,结果为何不堪入目呢?
无法寻找到答案,也无从知晓答案。不会有人告诉我为什么,因为过程并不重要;成绩❌和暑期实习❌的结果已经说明了一切。
有点累,不想码字了,先睡吧,如果能睡着的话。有可能的话,我好想重新开始一切,平平凡凡地走过人生。或者是有可能的话,重来一次太累,不如一劳永逸地结束吧。
讨厌现在的自己
讨厌现在的生活
早上起床头有些疼,布洛芬又救了我一命。感谢止疼药 💊
然后补完了报告中属于自己的部分。剩下的格式调整和补充等到组内的其他小伙伴完成后再进行修改。这个学期也即将随着夏天的到来而落下帷幕。
然后还是一样先做每日一题。Coding 需要保持手感,否则很快就萎缩了 😵💫
今天是贪心 + 栈。一开始对于贪心的设计不足导致出错,后来发现不能按照单调的方式去想,贪心应该贪心到剩余字符中最小的字符才可以。感觉好像有些晕晕的~
C++
class Solution {
public:
string robotWithString(string s) {
int n = s.length();
vector<char> min_str(n+1);
min_str[n] = 'z';
for(int i = n - 1; i >= 0; i--) {
min_str[i] = min(min_str[i+1], s[i]);
}
stack<char> st;
string ans;
for(int i = 0; i < n; i++) {
st.push(s[i]);
while(!st.empty() && st.top() <= min_str[i+1]) {
ans.push_back(st.top());
st.pop();
}
}
return ans;
}
};
X: @ArameDraw

IO 


今天忙的事情太多啦,明天早上更 🥺
IO 为啥会有人信和传播这些不切实际的胡说八道,为了泄愤罔顾事实?
阿根廷生存压力可比国内大多了,那些踢足球的酒囊饭袋工资比欧洲都高
纯粹是对足球本身没啥兴趣,能捞到钱就躺
2025 年 6 月 6 日 星期五 天气:多云
昨天太乱了把事情都忘了~
上午:https://xjtu.app/t/topic/14653/65
下午上完最后一节数据库课程,大学的所有必修课程也就全部结束了。暑期的实践也只有实验室的项目,其他意向的校外实习全部挂掉了~
随后校内的夏令营开了,就跑来跑去准备材料,但是应该不会报上的吧~其他大学的开放交流日也杳无音讯,一些学院需要的推荐信啥的也什么都没有呢,所以也不需要抱太大的希望。然后晚上 23 点的时候小组同学说报告的格式崩掉了。于是又去想着怎么修格式去了。
目前的计划还是一样按照 https://xjtu.app/t/topic/14653/3 来慢慢推进。一直努力到最后的时刻,之后的路只能且走且看,珍惜自己本科的最后一段时光吧。
愿我们都能获得自己想要的未来。
X: @ArameDraw

fight on なのです!
跑之前没有清鞋里的沙子,痛~

アイラちゃん 随后校内的夏令营开了,就跑来跑去准备材料
我今天刚看到 想着报不上就准备放弃了 把目前在联系的其他学校老师的考核认真做一做
好几天没看 lz 的日记了,感觉我日记没有这么 e 的原因只是单纯的因为土塬有认识我的校友所以不是很愿意彻底在这里解刨自己(笑
总之觉得 lz 已经很优秀了,也很感谢 leetcode 题单的分享
Zerick lz 也是,一边准备夏令营一边准备考研~
一起加油哦,我一直相信大老师说过的话:
努力不会背叛自己,梦想才会。
尽力做好所有的准备后,结果顺其自然。尽人事,听天命就是这个道理。烦恼和伤心,找一个地方记录下来,也算是一种倾泻的方式。
一步步向前走就好了 ![]()
今天可以好好 🐖
顺便奖励自己结束了大三学年

アイラちゃん 盲猜四餐 🐖
神秘小维 等有钱了再去外边好好 🐖 😤
2025 年 6 月 7 日 星期六 天气:小雨
今天考完了语文和数学,语文作文根本不会写,数学大题没做完,没有大学上了 QAQ
上午学习了相关跳表的数据结构,并且完成了基本的跳表操作,以及在整个表层面加入读写锁。对于 C++,我们在表结构内使用 std::shared_mutex 定义读写锁。采用 std::unique_lock 可以完成排他锁加锁工作,也就是写锁。写锁不允许除了持锁线程之外的锁进入到数据结构区域。对于读取操作,我们采用 std::shared_lock 加锁,也就是共享锁。共享锁允许多个仅进行读取操作的线程读入。一般来说是这样的:
简单实例
template<typename T>
class DataStructure {
public:
// Data Definition start
// ********
// Data Definition end
std::shared_mutex rwlock;
template<typename T>
T read() {
std::shared_lock rlock(rwlock); // Read operations with shared_lock;
// Read Operations;
}
template<typename T>
void write() {
std::unique_lock wlock(rwlock); // Write operations with exclusive_lock.
// Write operations;
}
};
对于多线程和 C++ 锁的 RAII 机制,以及原子操作和 CAS 方法还需要进一步地学习和实践。跳表的相关总结也需要在 Jun 8, 2025 (Asia/Shanghai) 尝试完成。
下午就是刷 leetcode。
3170. 删除星号以后字典序最小的字符串 (2025 年 6 月 7 日) (1772M)
这道题目的本质是贪心算法。对于贪心的设计不充分让主包感觉到了非常吃力,后面在图论结束后需要重点进攻 DP 和贪心。(80w vs 60w, 优势在我!)
并且通过这一题还尝试了 ranges 相关函数。
C++
#include<ranges>
class Solution {
public:
string clearStars(string s) {
vector<stack<int>> indices(26, stack<int>());
int count = 0;
for(int i = 0; i < s.length(); i++) {
if(s[i] == '*') {
for(int i = 0; i < 26; i++) {
if(!indices[i].empty()) {
s[indices[i].top()] = '*';
indices[i].pop();
break;
}
}
} else indices[s[i] - 'a'].push(i);
}
return s | ranges::views::filter([](char i) {return i != '*';})
| ranges::to<std::string>();
}
};
接下来就是图论部分:内向基环树。这类数据结构一般出现在每个节点均只有单个出边的有向无环图中,这样每个连通块有且仅有一个环,使得求取最长环成为可能。一般通过拓扑排序去除非环节点后进行。
典型的内向基环树问题。
C++
class Solution {
public:
int longestCycle(vector<int>& edges) {
// TopoSort to modify the graph.
int n = edges.size();
vector<int> inDegree(n, 0);
for(int i = 0; i < n; i++) {
if(edges[i] != -1) inDegree[edges[i]]++;
}
queue<int> q;
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) {
q.emplace(i);
}
}
while(!q.empty()) {
int cur = q.front();
q.pop();
int adj = edges[cur];
edges[cur] = -1;
if(adj != -1 && --inDegree[adj] == 0) {
q.emplace(adj);
}
}
int ans = -1;
auto dfs = [&](this auto && dfs, int from) -> int {
int nxt = edges[from];
if(nxt == -1) return 0;
edges[from] = -1;
return dfs(nxt) + 1;
};
for(int i = 0; i < n; i++) {
ans = max(ans, dfs(i));
}
return ans == 0 ? -1 : ans;
}
};
与其相反的,另一条最短环问题。对于最短环问题,我们需要逐个(假性)去除边,随后从该边的一个节点开始,进行 Dijkstra 或 floyd 最短路计算出该节点到另一个节点的最短距离,最后的最小环即为 。有向图则以出边为起点进行。具体可以查看:
https://oiwiki.com/graph/min-cycle/
C++
class Solution {
public:
int findShortestCycle(int n, vector<vector<int>>& edges) {
// Build graph.
vector<vector<int>> graph(n);
for(const auto & edge: edges) {
int u = edge[0], v = edge[1];
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
int * distance = new int [n];
// Dijkstra with delete the edge. For indirect graph, pick one side, run dijkstra.
// Directed should strictly follow start and end point.
auto bfs = [&](int u, int v) -> void {
queue<int> q;
q.emplace(u);
distance[u] = 0;
while(!q.empty()) {
int cur = q.front();
q.pop();
for(const auto & adj: graph[cur]) {
// False delete.
if(cur == u && adj == v) {
continue;
}
// Updated.
else if(distance[adj] < 0x7f7f7f7f) {
continue;
}
else if(distance[adj] > distance[cur] + 1) {
distance[adj] = distance[cur] + 1;
q.emplace(adj);
}
}
}
};
int ans = INT_MAX / 2;
for(const auto & edge: edges) {
memset(distance, 0x7f, sizeof(int) * n);
int u = edge[0], v = edge[1];
bfs(u, v);
ans = min(ans, distance[v] + 1);
}
return ans == INT_MAX / 2 ? -1 : ans;
}
};
最后遇到的 冗余连接 I 和 冗余连接 II 放到 Jun 8, 2025 (Asia/Shanghai) 在进行记录,都涉及到了并查集问题。
随后就是 running+ 🐖
http://xjtu.app/t/topic/14653/70
http://xjtu.app/t/topic/14653/73
晚上被讲解抗日战争的视频硬控了,强力推荐,讲的非常详细,于是偏离主力工作摸鱼~
在此向革命前辈和先烈们致敬,永垂不朽。
https://www.bilibili.com/video/BV1E6TTz9Eqy
今晚就是这样了~祝大家晚安好梦,明天高考继续旗开得胜!
アイラちゃん 有一次面试让手撕读写锁给我干碎了
アイラちゃん
auto dfs = [&](this auto && dfs, int from) -> int {
int nxt = edges[from];
if(nxt == -1) return 0;
edges[from] = -1;
return dfs(nxt) + 1;
};
居然是 cpp23
Zerick 拥抱新标准 🤗
手撕读写锁那很坏了,直接用 C++17 😤
🫨op 的刷题和学习效率都好高
上午算跳表的时间、空间复杂度浪费了好多脑细胞,アイラちゃん是 begg 所以请各路大神写论文的时候说的再详细一些吧 😭
有关跳表总结完成 https://www.cnblogs.com/mumujun12345/p/18919314
每日一题部分:
386. 字典序排数
很经典的 dfs 算法做一次字典序输出。
C++
class Solution {
public:
vector<int> lexicalOrder(int n) {
vector<int> ans;
auto dfs = [&](this auto && dfs, int start) -> void {
if(start > n) {
return;
}
ans.emplace_back(start);
dfs(start * 10);
for(int i = 1; i <= 9; i++) {
if(start + i <= n && (start + i) % 10 != 0) {
ans.emplace_back(start + i);
} else {
break;
}
dfs(10 * (start + i));
}
};
dfs(1);
return ans;
}
};
这论文真是越读越晕,好难理解啊 😵💫
お願い、能不能让 Begg 变聪明一点,我什么都会做的 😭


2025 年 6 月 8 日 星期日 天气:中雨转阴
上午:整理跳表,解决每日一题
http://xjtu.app/t/topic/14653/81
下午:继续看 conga 但是没有看完(只是解决了 conga 里的五个点:为什么分布式,为什么网络层部署,为什么全局感知,为什么小流检测,为什么叶子交换机之间反馈机制)。只能慢慢看了,很复杂,感觉几天都看不完。 😭https://www.cnblogs.com/mumujun12345/p/18906167
晚上发现同学的项目用 makefile 构建,源文件和编译中间文件混杂在一起,就开了一个 pull request,写了一个 cmakefile 来改进编译系统。
是一个很小的 sysY 编译器项目,原来的 makefile 就是寻找 src 内的所有 c,cc 和 cpp,并且头文件和 cpp 文件放在了一起,因此也没有必要去寻找 include 文件夹。直接暴力递归搜索:
file(GLOB_RECURSE SOURCES
"src/*.c"
"src/*.cpp"
"src/*.cc"
)
add_executable(compiler ${SOURCES})
target_include_directories(compiler PUBLIC src)
原来的 makefile 采用 g++ 编译了 c 语言文件,因为我无法得知采用 gcc 编译后是否会出现链接问题,所以强行改变文件的格式,使得 g++ 来进行编译。
# For each C file, get them compile with g++.
foreach(SOURCE ${SOURCES})
if(SOURCE MATCHES "\\.c$")
set_source_files_properties(${SOURCE} PROPERTIES LANGUAGE CXX)
endif()
endforeach()
最后设定头文件搜索文件夹地址,以及编译形成的可执行文件(项目)。
# add executable, the final ./compiler.
add_executable(compiler ${SOURCES})
target_include_directories(compiler PUBLIC src)
最后不要忘记在项目开始设定编译选项,指定 g++,输出 compile_commands.json 给 clangd 做索引分析~
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall")
set(CMAKE_EXPORT_COMPILE_COMMANDS true)
这样就成功地赚到了一个 PR,并被 merge~

新的一周即将开始,祝大家工作、学习顺利,晚安~
X:@PhD_illust

等有了小钱,就租一个小窝就好了
也不是说舍友不好,只是想有一个自己安静的小窝
アイラちゃん 一间自己的房间! ![]()
水源账号消失了 🥺
i-BuProfen 水源带给我的焦虑感和矛盾感比较强,所以暂时销号了 🥺 抱歉
今天打算主攻 leetcode。今天的题目是十叉树,其衍生的字典树还没有学习 😵💫,于是开启 cv 法阵,所以要找时间补题了。
C++
class Solution {
public:
int getSteps(int curr, long n) {
int steps = 0;
long first = curr, last = curr;
while(first <= n) {
steps += min(last,n) - first + 1;
first *= 10;
last = last * 10 + 9;
}
return steps;
}
int findKthNumber(int n, int k) {
int curr = 1;
k--;
while(k > 0) {
int steps = getSteps(curr, n);
if(steps <= k) {
k -= steps;
curr++;
} else {
curr = curr * 10;
k--;
}
}
return curr;
}
};
并查集。并查集的核心点其实就是路径压缩和按秩合并(按大小合并)。两者均可以到达一个较好的时间复杂度,时间复杂度的分析比较复杂,刷 leetcode 以实用为主,就不去分析了。
贴上模板 (按大小合并)~
C++ union template
class Union {
public:
vector<int> parent;
vector<int> size;
int components;
// The Costruction function.
Union(int n):
parent(vector<int>(n)),
size(vector<int>(n,1)),
components(n)
{
iota(parent.begin(), parent.end(), 0);
}
// Path compression. Find parent until the same.
int find(int id) {
if(parent[id] != id) {
parent[id] = find(parent[id]);
}
return parent[id];
}
// Merge by size. You can also make it by rank, for modified as size[pa_i] += 1;
void merge(int id1, int id2) {
// Find parent.
int pa1 = find(id1);
int pa2 = find(id2);
if(pa1 == pa2) {
return;
}
if(size[pa1] < size[pa2]) {
parent[pa1] = pa2;
size[pa2] += size[pa1];
}
else {
parent[pa2] = pa1;
size[pa1] += size[pa2];
}
if(pa1 != pa2)
components--;
}
bool judge(int id1, int id2) {
return find(id1) != find(id2);
}
};
可以用来计算连通分量。以下是前面挖的坑,今天补上。
无向图只需要对边进行遍历,判断这条边会不会把位于同一个并查集的边连接起来,如果出现则直接返回。
想到了什么?对的 Kruskal 算法也是这么做的,生成最小生成树,首先贪心地选择边权最小的边,判断两端点是否在同一个并查集中。
C++
inline int find(vector<int> & parent,int id) {
if(id != parent[id]) {
return find(parent,parent[id]);
}
return parent[id];
}
inline void merge(vector<int> & parent,vector<int> & size, int id1, int id2) {
int pa1 = find(parent,id1);
int pa2 = find(parent,id2);
if(pa1 == pa2) {
return;
}
// small merge to big.
if(size[pa1] < size[pa2]) {
parent[pa1] = pa2;
size[pa1] += size[pa2];
} else {
parent[pa2] = pa1;
size[pa2] += size[pa1];
}
}
inline bool judge(vector<int> & parent,int id1, int id2) {
return find(parent,id1) == find(parent,id2);
}
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<int> parent(static_cast<int>(edges.size() + 1));
iota(parent.begin(), parent.end(), 0);
vector<int> size(static_cast<int>(edges.size() + 1), 1);
vector<int> ans;
for(const auto & edge: edges) {
int u = edge[0], v = edge[1];
if(!judge(parent, u, v)) {
merge(parent, size, u,v);
}
else {
ans={u,v};
break;
}
}
return ans;
}
};
题目有些难,看了题解才知道怎么做。主要的就是寻找汇点(因为每个节点只允许拥有一个父节点)和环路边。当两者均出现的时候,删除环路边。仅有冲突边的时候,删除最后一个冲突边。仅有环路的时候,删除环路最后一条边。针对一个节点不会出现两条以上的冲突边和环路边,因为题目保证了(卡在这里想了很久) 🐷

C++
class Union {
public:
int * ancestors;
int * size;
int n;
public:
Union(int n):n(n) {
ancestors = new int [n];
size = new int [n];
for(int i = 0; i < n; i++) {
ancestors[i] = i;
size[i] = 1;
}
}
int find(int id) {
if(id != ancestors[id]) {
ancestors[id] = find(ancestors[id]);
}
return ancestors[id];
}
void merge(int id1, int id2) {
int pa1 = find(id1);
int pa2 = find(id2);
if(pa1 == pa2) {
return;
} else if(size[pa1] > size[pa2]) {
size[pa1] += size[pa2];
ancestors[pa2] = pa1;
} else {
size[pa2] += size[pa1];
ancestors[pa1] = pa2;
}
}
bool judge(int id1, int id2) {
return find(id1) == find(id2);
}
};
class Solution {
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int n = edges.size();
Union un(n+1);
int conflict = -1; // Mark conflict apperance.
int loop = -1;
int * parents = new int [n+1];
for(int i = 0; i < n + 1; i++) {
parents[i] = i;
}
// Two parent: Conflict.
// Same ancestor
for(int i = 0; i < n; i++) {
int u = edges[i][0], v = edges[i][1];
if(parents[v] != v) {
conflict = i;
} else {
parents[v] = u;
int pa_u = un.find(u);
int pa_v = un.find(v);
if(pa_u == pa_v) {
loop = i;
} else {
un.merge(u, v);
}
}
}
if(conflict == -1) {
return edges[loop];
} else {
int u = edges[conflict][0], v = edges[conflict][1];
if(loop == -1) {
return edges[conflict];
} else {
return vector<int>{parents[v], v};
}
}
}
};
X: @kasuga_iz

2025 年 6 月 9 日 星期一 天气:阵雨
上午提要~:http://xjtu.app/t/topic/14653/88
本来想在 leetcode 上一展宏图,没想到被拉去做服务器运维去了 😭 记录一下今天踩的坑 🐷
前情提要
在做好一切 ssh 配置后,进行最后的维护检查,htop 时发现了 64 核 cpu 全部 100% 占用。第一反应是查看运行进程,发现是大量不具名的
--bash进程。
进行断网处理后,发现进程不再继续进行。断开所有 ssh 连接后,重新恢复网络连接,通过 wireshark 发现了一个非常可疑的发往 143.110.58.7 的网络包(实际挖矿 IP 代理有美国,欧洲和中国香港 IP),通过 80 端口发出,随后进行三次握手后,所有 cpu 开始饱满。通过sudo ufw deny 80/tcp后杀死进程不会再重启。否则会在一段时间后重启。立刻怀疑服务器已经被植入挖矿脚本。随后证实了如下情况。
通过日志得知该进程在嗅探过程中发现本机 22 端口完全不设防并且密码极其简单,采用暴力方式破解后通过 ssh 植入了后门。只可惜没有做的太干净,还是被我发现了 😅
服务器的密码设置的太简单,又通过内网穿透暴露到了外网,被端口扫描发现并植入了恶意挖矿脚本,进行全天候 24 小时挖矿,并且夺取了系统的 root 权限。于是我们只得重新安装 bios 和操作系统。
今天师傅重新安装了 BIOS。于是我们重新安装 ubuntu22.04 到服务器上。我们的服务器共两台,每台是 64 核 CPU,x86_64 架构,CPU 是很不错的 AMD Ryzen Threadripper PRO 7975WX 32-Cores,是很先进的 cpu~,cpu 支持 52 位实地址,57 位虚地址。感兴趣的小伙伴可以看看,偷偷打印了一份 CPU 的报告 😋
cpu.txt|attachment (3.6 KB)
然后 GPU 是 L40,两机各两台。通过 nvidia 的 bluefield 的 DPU,支持 InfiniBand,RocE 两种 RDMA 协议,可以让两卡通信数据绕过 cpu 内核直接写入内存。之前还出现过高温的情况(DPU 烧到了接近 100 度 😭),不过后面还是通过加装更多的风扇解决了。
开始处理。首先基本的系统安装完成后,开始装载我们的 7.3T 的硬盘和 NvMe 硬盘。担心原先的硬盘也被动了手脚,于是我重新进行了分区,格式化硬盘里的所有数据,在 root 路径下创建一个新的文件夹,并将其挂载。
mklabel gpt
mkpart primary 1 -1
p # output
sudo mkfs.ext4 /dev/sda # 格式化硬盘
手动挂载会导致服务器重新上电后不自动挂载。因此我们将挂载规则写入 etc/fstab

/dev/sda /disk1 ext4 defaults 0 0
这样每次都会将我们的硬盘以 ext4 格式挂载到/disk1 路径下。
接下来还需要扩展系统盘。默认的系统盘安装在 NvMe 后,只会分配 100GB 的空间。我们将 NvMe 硬盘分成了 3 个分区,前两个分区用于
- /boot(目录用于存储启动 Linux 系统所需的核心文件,包括内核文件(如 vmlinuz),初始 RAM 磁盘映像(如 initrd 或 initramfs),以及用于启动加载程序(如 GRUB)的配置文件);
- /boot/efi(一个可扩展固件接口,用于efi启动器的挂载点,包含了efi版本的grub);
最后一个分区被用来整个作为系统盘。我们于是:
sudo vgdisplay # 查看各个盘,分区和大小
lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv # 记不清名字啦
resize2fs dev/mapper/ubuntu--vg-ubuntu--lv # 重新调整大小进行扩容
这样就成功扩容系统盘大小。效果图如下:

接下来解决启动过程中等待时间过长问题,经过排查得知是强行等待网络连接直至 2min30s 超时导致,于是手动修改 systemd-networkd-wait-online.service 规则。
cd /etc/systemd/system/network-online.target.wants/
sudo vim systemd-networkd-wait-online.service
在 [Services] 一栏添加 TimeoutSec=5,强制超时 5s 后立刻启动。

这样无需等待启动。
此时另一位同学加入战场,于是失去了学习配置 ssh 公钥私钥的机会,去安装相关 nvidia 驱动,cuda 编译相关工具。
首先安装 C/C++ 相关编译工具链。
sudo apt update && sudo apt install build-essentials
安装 gcc-11 和 g++-11 版本适配 Ubuntu22.04. 接下来安装 nvidia 相关驱动,nvidia-smi 显示相关驱动为 570 版本。
sudo ubuntu-drivers devices
sudo ubuntu-drivers autoinstall
成功安装 nvidia 相关驱动。接着安装相关工具链,这也很简单,也就是sudo apt install nvidia-cuda-toolkit
很顺利,和第一次一样成功安装。

接下来进行 gpu 压力测试,我们采用最经典的 gpu-burn。
wget https://codeload.github.com/wilicc/gpu-burn/zip/master # 下载
unzip gpu-burn-master.zip # 解压缩
cd gpu-burn-master
make
形成 gpu-burn 可执行文件。基于之前的经验,采用 double 运算无法让 GPU 达到功率墙,因为 GPU 最擅长的不是双精度浮点运算,而是单精度 float。因此进行压测,采用 90% 的 memory 占用,压测 5 分钟。五分钟满载测试,GPU 温度不超过 75 度。成功。
./gpu-burn -tc -m 90% 300 # tc 启用 tensorCore

接下来检查 cpu 是否在上次的挖矿中受损。因此采用 stress --cpu 64 对全部 cpu 进行压测。温度在 85 摄氏度左右,表现正常。

此时同学已经将公,私钥配好并进行分发。于是在配置好中央 windows 跳板机后,利用跳板机配置代理转发功能,实现跳板机登录服务器。首先将私钥存入本机中。/User/xxx/.ssh/id_jump 中。随后在 vscode 内写入规则。
Host 这里是服务器名
HostName 这里是 IP
User 这里是用户名
Port 这里是 ssh 端口
ProxyJump 这里是跳板机
IdentityFile /Users/xxx/.ssh/id_jump // 私钥地址
Host 跳板机名
HostName IP
User 用户名
Port 端口
IdentityFile /Users/xxx/.ssh/id_jump // 私钥地址
这样就实现了一键无密钥登录服务器。仅有跳板机位于公网,且安装了强力的杀毒软件(是的,360,以毒攻毒),封锁了 80(HTTP),3389(远程协助),443(HTTPS),22(ssh) 等大量专用端口,并使用公私钥进行 ssh 登录,不让坏人具有任何可乘之机。
于是经过一个下午的忙碌,服务器终于被抢救回了原来的模样。这件事也时刻告诫我:网络安全不是儿戏,要更加认真地担负起责任。
祝大家晚安~
🌃
X: @chun_paretto

アイラちゃん 服务器的密码设置的太简单,又通过内网穿透暴露到了外网,被端口扫描发现并植入了恶意挖矿脚本,进行全天候 24 小时挖矿,并且夺取了系统的 root 权限。
好像校内不少服务器都难逃此劫啊。
PipaQinse233 对的,挖矿行动很猖獗 🥲
アイラちゃん 刚才顺手检查了一下自己的小服务器 (给寝室以及行政班里的 Minecraft 服务器用的),发现 ssh 登陆失败的那几条全是因为自己忘记了密码🤣,估计是因为内网穿透只暴露了俩端口且穿透后只能校内访问,才逃过一劫吧。
不过好像也有校内主机感染木马病毒、进一步扫描爆破其他主机的说法,不确定现在还有没有。
PipaQinse233 还是要做好防护,小服务器不需要担心太多,只需要封锁该封的端口,ssh 尽量走公私钥,像大服务器还要做负载冗余,容灾,博客园最近一直再收到 DDOS 攻击,今晚好像又瘫痪了。阿里云的云服务好像也前几天早上瘫痪过,导致我都看不了我的 blog 了 😇
现在还是 502 bad gateway,经典的超负荷情况,博客园又买不起昂贵的 cdn,血都被 csdn 吸干了
今天中午打算回去打扫一下宿舍,不然真的脏的没救了
今天中午怎么宿舍人都在,以前可不是这样的
等到下午他们走了再打扫不然打扫不干净
アイラちゃん
IO 阿里云是域名劫持吗,这我就不清楚了
博客园昨天确实是 DDOS
向图论大神学习,每天坚持 leetcode!
主治医师李大华 ❌️
并非大神,是抱佛脚而已 🥲
Leetcode 日~
拓扑排序找出环并记录环长度,dfs 记录链表长度并剪枝即可,因为题目帮助我们排除了多出边的情况。
C++
class Solution {
public:
vector<int> countVisitedNodes(vector<int>& edges) {
int n = edges.size();
queue<int> q;
int * inDegree = new int [n];
int * mark = new int [n]; // Mark vertex as loop node = 0 or as chain node. chain = 1.
vector<int> ans(n, 1);
memset(inDegree, 0, sizeof(int) * n);
memset(mark, 0, sizeof(int) * n);
// O(n)
for(int i = 0; i < n; i++) {
inDegree[edges[i]]++;
}
// O(n)
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) {
q.emplace(i);
}
}
while(!q.empty()) {
int cur = q.front();
mark[cur] = 1;
q.pop();
if(--inDegree[edges[cur]] == 0) {
q.emplace(edges[cur]);
}
}
int loop = 1;
auto dfs = [&](this auto && dfs, int from) -> void {
int next = edges[from];
if(mark[next] == 2 || next == -1) {
mark[from] = 2;
ans[from] = loop;
return;
}
edges[from] = -1;
mark[from] = 2; // visited.
loop++;
dfs(next);
ans[from] = loop;
};
for(int i = 0; i < n; i++) {
if(mark[i] == 1 || mark[i] == 2) continue;
loop = 1;
dfs(i);
}
int start, chain;
auto dfs2 = [&](this auto && dfs, int start) -> void {
int next = edges[start];
if(mark[next] == 2 || edges[next] == -1) {
chain += ans[next];
ans[start] = chain;
mark[start] = 2;
return;
}
mark[start] = 2;
dfs(next);
ans[start] = ++chain;
};
for(int i = 0; i < n; i++) {
if(mark[i] == 2) continue;
start = i;
chain = 1;
dfs2(start);
}
return ans;
}
};
进入 dijkstra 算法。
代表性题目,我们用 C 搓一下轮子。
C
typedef struct Node {
int v;
int w;
struct Node * next;
} Node;
void node_init(Node * obj, int to, int weight) {
obj -> v = to;
obj -> w = weight;
obj -> next = NULL;
}
typedef struct DiGraph {
int n;
Node ** headers;
Node ** tails;
} DiGraph;
void BuildGraph(DiGraph * obj, int n, int ** times, int timesSize) {
obj -> n = n;
obj -> headers = (Node **) malloc(sizeof(Node *) * n);
obj -> tails = (Node **) malloc(sizeof(Node *) * n);
for(int i = 0; i < n; i++) {
obj -> headers[i] = (Node *) malloc(sizeof(Node));
node_init(obj -> headers[i], i, 0);
obj -> tails[i] = obj -> headers[i];
}
for(int i = 0; i < timesSize; i++) {
int u = times[i][0] - 1, v = times[i][1] - 1, w = times[i][2];
obj -> tails[u] -> next = (Node *)malloc(sizeof(Node));
obj -> tails[u] = obj -> tails[u] -> next;
node_init(obj -> tails[u], v, w);
}
}
typedef struct min_heap {
int n;
int size;
Node * node;
} min_heap;
void init_min_heap(min_heap * obj, int n) {
obj -> n = n;
obj -> size = 0;
obj -> node = (Node *) malloc(sizeof(Node) * n);
}
int parent(int i) {
return (i + 1) / 2 - 1 > 0 ? (i + 1) / 2 - 1 : 0;
}
int left_child(int i) {
return i * 2 + 1;
}
int right_child(int i) {
return (i + 1) * 2;
}
void min_heapify(min_heap * obj, int i) {
if(i >= obj -> size) {
return;
}
int lchild = left_child(i);
int rchild = right_child(i);
// swap the smallest up.
int smallest = i;
if(lchild < obj -> size && obj -> node[lchild].w < obj -> node[smallest].w) {
smallest = lchild;
}
else smallest = i;
if(rchild < obj -> size && obj -> node[rchild].w < obj -> node[smallest].w) {
smallest = rchild;
}
if(smallest != i) {
Node temp = obj -> node[i];
obj -> node[i] = obj -> node[smallest];
obj -> node[smallest] = temp;
min_heapify(obj, smallest);
}
}
Node top(min_heap * obj) {
return obj -> node[0];
}
void pop(min_heap * obj) {
if(obj -> size == 0) {
return;
}
Node minimum = obj -> node[0];
obj -> node[0] = obj -> node[obj -> size - 1];
obj -> size--;
min_heapify(obj, 0);
}
void push(min_heap * obj, Node node) {
obj -> size++;
obj -> node[obj -> size - 1] = node;
for(int i = obj -> size - 1; i >= 0; i = parent(i)) {
if(obj -> node[parent(i)].w > obj -> node[i].w) {
Node tmp = obj -> node[parent(i)];
obj -> node[parent(i)] = obj -> node[i];
obj -> node[i] = tmp;
}
else break;
}
}
char empty(min_heap * obj) {
return obj -> size == 0;
}
int networkDelayTime(int** times, int timesSize, int* timesColSize, int n, int k) {
DiGraph * digraph = (DiGraph *) malloc(sizeof(DiGraph));
BuildGraph(digraph, n, times, timesSize);
min_heap * test_heap = (min_heap *) malloc(sizeof(min_heap));
init_min_heap(test_heap, timesSize);
int * visited = (int *) malloc(sizeof(int) * n);
int * distance = (int *) malloc(sizeof(int) * n);
memset(visited, 0, sizeof(int) * n);
memset(distance, 0x7f, sizeof(int) * n);
visited[k - 1] = 1;
distance[k - 1] = 0;
Node cur;
node_init(&cur, k - 1, 0);
push(test_heap, cur);
while(!empty(test_heap)) {
cur = top(test_heap);
pop(test_heap);
int cur_v = cur.v, cur_w = cur.w;
for (Node * adj = digraph -> headers[cur_v]; adj != NULL; adj = adj -> next) {
int adj_v = adj -> v, adj_w = adj -> w;
if(distance[adj_v] > distance[cur_v] + adj_w) {
distance[adj_v] = distance[cur_v] + adj_w;
Node tmp;
node_init(&tmp, adj_v, distance[adj_v]);
push(test_heap, tmp);
}
}
}
int ans = 0;
for(int i = 0; i < n; i++) {
ans = ans < distance[i] ? distance[i] : ans;
}
return (ans == 0x7f7f7f7f) ? -1 : ans;
}
只需要判断更新前最短路是否大于消失时间即可。最后用 ranges 转换一下。
C++
#include<ranges>
class Solution {
public:
vector<int> minimumTime(int n, vector<vector<int>>& edges, vector<int>& disappear) {
vector<vector<pair<int,int>>> graph(n);
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].emplace_back(v,w);
graph[v].emplace_back(u,w);
}
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
pq.emplace(0,0);
vector<int> distance(n, INT_MAX);
vector<int> visited(n, 0);
distance[0] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(visited[cur]) continue;
visited[cur] = 1;
for(const auto & [adj, w]: graph[cur]) {
if(disappear[adj] > w + distance[cur] && distance[adj] > w + distance[cur]) {
distance[adj] = distance[cur] + w;
pq.emplace(distance[adj], adj);
}
}
}
return distance | ranges::views::transform([](int i){
return (i == INT_MAX) ? -1 : i;
}) | ranges::to<vector<int>>();
}
};
将加法改成乘法。
C++
class Solution {
public:
double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
vector<vector<pair<double, int>>> graph(n);
for (int i = 0; i < edges.size(); i++) {
int u = edges[i][0], v = edges[i][1];
double w = succProb[i];
graph[u].emplace_back(w, v);
graph[v].emplace_back(w, u);
}
priority_queue<pair<double, int>> pq;
vector<double> distance(n, 0);
vector<int> visited(n, 0);
pq.emplace(1, start);
distance[start] = 1;
while (!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if (visited[cur]) {
continue;
}
visited[cur] = 1;
for (auto& [w, adj] : graph[cur]) {
if (distance[adj] < distance[cur] * w) {
distance[adj] = distance[cur] * w;
pq.emplace(distance[adj], adj);
}
}
}
return distance[end];
}
};
本来这道题不难的,就是 dijkstra+ 图上 DP,DP 的方式也就是
但是进行 dfs 过程中アイラちゃん错误地初始化 dp 数组为 0,最终会导致一部分剪枝失败(当一个点的所有邻接点均不可以 dfs 时,dp[i]=0, 这样下一次遇到就会反复重新进行该点邻接点的遍历),从而导致记忆化搜索失败。寻找这个问题耗费了接近 1h 😵💫
C++
constexpr int MOD = 1e9 + 7;
class Solution {
vector<vector<pair<int,int>>> graph;
vector<int> distance, visited, dp;
int num;
public:
int dfs(int cur) {
if(cur == num) {
return 1;
} else if(dp[cur] != -1) {
return dp[cur];
}
int ans = 0;
for(const auto & adj: graph[cur]) {
int nxt = adj.first;
if(distance[cur] > distance[nxt]) {
ans = (ans + dfs(nxt)) % MOD;
}
}
dp[cur] = ans;
return ans;
}
int countRestrictedPaths(int n, vector<vector<int>>& edges) {
num = n;
graph = vector<vector<pair<int,int>>>(n + 1);
// 1 as start index.
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].emplace_back(v,w);
graph[v].emplace_back(u,w);
}
priority_queue<pair<int, int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
pq.emplace(0, n);
distance = vector<int>(n+1, INT_MAX);
visited = vector<int>(n+1, 0);
distance[n] = 0;
// Dijkstra started from last node.
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(visited[cur] == 1) continue;
visited[cur] = 1;
for(const auto & [adj, w]: graph[cur]) {
if(distance[adj] > distance[cur] + w) {
distance[adj] = distance[cur] + w;
pq.emplace(distance[adj], adj);
}
}
}
// Distance to LastNode complete.
dp = vector<int>(n+1, -1);
dp[n] = 1;
return dfs(1);
}
};
X: @fuumiisc

要再接再厉。
良辰吉时已到,宿舍无人。
环境之脏乱已难以忍受,开干!
清扫完成~这宿舍之前真的是狗窝
アイラちゃん 看看干净的宿舍()
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 宿舍



清除垃圾

有些陈年水垢无法清除了
我从不点外卖,因为太贵了
刚刚洗完澡地也干了
アイラちゃん 你们居然还是独立卫生间吗
是东区吗🤔
神秘小维 📦️
![]()
其实我也拍了清洗前的宿舍,但是有碍国际观瞻,就不放出来了
![]()
アイラちゃん 看到这个 忽然也不羡慕东区独立卫浴了 这个设施看起来好旧啊
以及 lz 的清洁能力好强 (我也想要这样的室友)
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 不要羡慕,厕所地面高于宿舍,清洗起来很麻烦的 😭
アイラちゃん
这……让我想到了某些不好的画面
下午开会 + 开会 + 摸鱼 😇
2025 年 6 月 11 日 星期三 天气:阴/阵雨
今天除了上午刷了 leetcode,下午就是开组会 + 开专业毕业会 + 开夏令营宣讲会,合理摸鱼日~
🐟️
今天的题目都稍微比较难,为了调超时都很难受 😵💫
这是一道前缀和 + 滑动窗口题目,アイラちゃん一直很不懂怎么很好地写双指针,因此每次都调试很久+cv 🥺
C++
class Solution {
public:
int maxDifference(string s, int k) {
const int inf = INT_MAX / 2;
int ans = -inf;
for (int x = 0; x < 5; x++) {
for (int y = 0; y < 5; y++) {
if (y == x) {
continue;
}
int cur_s[5]{}, pre_s[5]{};
int min_s[2][2] = {{inf, inf}, {inf, inf}};
int left = 0;
for (int i = 0; i < s.size(); i++) {
cur_s[s[i] - '0']++;
int r = i + 1;
while (r - left >= k && cur_s[x] > pre_s[x] && cur_s[y] > pre_s[y]) {
int& p = min_s[pre_s[x] & 1][pre_s[y] & 1];
p = min(p, pre_s[x] - pre_s[y]);
pre_s[s[left] - '0']++;
left++;
}
ans = max(ans, cur_s[x] - cur_s[y] - min_s[cur_s[x] & 1 ^ 1][cur_s[y] & 1]);
}
}
}
return ans;
}
};
直接走 dijkstra+dfs 将会超时。因为可以构造一个长链连接到最后的全连接图上面,全连接图的 dfs 遍历将出现指数级复杂度,从而超时。
当然也可以用 dijkstra 算法,从 0 计算一次,再从 n-1 计算一次,最后判断 是否成立就可以了。
C++
using namespace std;
using pii = pair<int,int>;
using tiii = tuple<int,int,int>;
using Graph = vector<vector<tiii>>;
#define RELEASE
class Solution {
public:
vector<bool> findAnswer(int n, vector<vector<int>>& edges) {
// Build graph here.
Graph graph(n);
int ind = 0;
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].emplace_back(v,w,ind);
graph[v].emplace_back(u,w,ind);
ind++;
}
// iterate the graph now.
vector<int> distance(n, INT_MAX / 2);
vector<int> visited(n, 0);
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.emplace(0,0);
vector<vector<int>> pre(n); // Record the pre vertex of the graph.
distance[0] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(visited[cur] == 1) continue;
visited[cur] = 1;
// Iterate over the adjacent vertices.
for(const auto & [adj, w, _]: graph[cur]) {
if(distance[adj] > dist + w) {
distance[adj] = distance[cur] + w;
pq.emplace(distance[adj], adj);
}
}
}
vector<char> visited2(n, 0);
vector<bool> ans(edges.size(), false);
auto dfs = [&](this auto && dfs, int from) -> void {
visited2[from] = 1;
for(const auto & [adj, w, index]: graph[from]) {
if(distance[adj] + w != distance[from]) {
continue;
}
ans[index] = true;
if(!visited2[adj]) {
dfs(adj);
}
}
};
if(distance[n-1] == INT_MAX / 2) return ans;
dfs(n-1);
return ans;
}
};
同样 dijkstra+dfs,稍微更改一下上面的就可以了。
要记得开 long long
C++
using plli = pair<long long,int>;
using Graph = vector<vector<pair<int,int>>>;
using ll = long long;
constexpr int MOD = 1e9+7;
#define RELEASE
class Solution {
public:
int countPaths(int n, vector<vector<int>>& roads) {
// Build graph here.
Graph graph(n);
for(const auto & edge: roads) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].emplace_back(v,w);
graph[v].emplace_back(u,w);
}
// iterate the graph now.
vector<ll> distance(n, LLONG_MAX);
vector<int> visited(n, 0);
priority_queue<plli, vector<plli>, greater<plli>> pq;
pq.emplace(0,0);
distance[0] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(visited[cur] == 1) continue;
visited[cur] = 1;
// Iterate over the adjacent vertices.
for(const auto & [adj, w]: graph[cur]) {
if(distance[adj] > (ll)dist + w) {
distance[adj] = (ll)distance[cur] + w;
pq.emplace(distance[adj], adj);
}
}
}
#ifdef DEBUG
for(int i = 0; i < n; i++) {
const auto & adj = pre[i];
cout << i << ": ";
for(int i = 0; i < adj.size(); i++) {
cout << adj[i] << ' ';
}
cout << endl;
}
#endif
if(distance[n-1] == LLONG_MAX) return 0;
vector<int> counts(n, -1);
auto dfs = [&](this auto && dfs, int from) -> int {
if(from == 0) {
counts[from] = 1;
return 1;
} else if(counts[from] > -1) {
return counts[from];
}
counts[from] = 0;
for(const auto & [adj, w]: graph[from]) {
if(distance[adj] + w == distance[from]) {
counts[from] = (counts[from] + dfs(adj)) % MOD;
}
}
return counts[from];
};
return dfs(n-1);
}
};
明天要给学长画图,visio 从未用过,看来得学一学 😵💫
明天不能再摆烂摸鱼了,要刷 leetcode+ 画图 + 复习高数,星期日去市区玩一玩 😋
🫳 🐟️ ❌️
アイラちゃん 🫳 🐟️ ❌️
在アイラちゃん投简历后接近一个月,突然发来了面试通知,到底是什么问题啊 😵💫 我还以为我初筛没过呢,突然跟我说初筛过了明天上午 10 点面试,我怎么抱佛脚啊 😭
JD:
对集群管理、机器学习平台、Kubernetes/Docker 有基本认识,熟悉 Linux 操作系统。乐于助人,耐心细致。计算机科学、软件工程等相关专业。熟练使用 Python/C++,掌握至少一种深度学习训练框架,具备 Megatron/DeepSpeed 框架研发经验者优先。
AI infra 实习
话说アイラちゃん是要保研考研还是就业哎(
此人不语,只是一味地在门里生产废料。
post deleted by author 保研>考研>就业 目前是这样
所以重心其实在前两者,所以刷 leetcode 防机考,目前一半复习数学 +408,剩下时间做 lab/忙实验室组里的东西
不太想卷,只想找个代码相关的工作最后,读硕士也是想攒实习经历 + 学习一些更多的技术
记录:对着 JD 抱 docker、k 哈基 s 的佛脚
不全面的面筋
Docker & k8s
Docker
-
运行于 OS 上的软件,用于创建,管理,编排容器。可以将开发的应用程序自动部署到容器。
-
与 VM 的区别
-
docker 应用层抽象,容器间通过网络命名空间进行隔离。多个容器共享一个 OS 内核。
-
VM 对物理硬件层抽象,包含独立操作系统。
-
前者为应用环境提供,后者为操作系统环境提供。
-
docker 组件
-
引擎(客户端,服务端),docker 镜像,容器,Registry(镜像仓库)
-
架构:Client(Application)-Server(OS)架构。
Docker 引擎
-
Docker 引擎主要有:docker 客户端,docker 守护进程 (daemon),containerd 和 runc。
-
现在的 docker 引擎架构:
-
docker client(CLI) 与 docker daemon(API 与其它特性)进行交互。
-
docker daemon 与容器的监督者 (supervisor) 进行交互,有 start|stop|pause...等管理容器。
-
containerd 负责启动没有守护进程的容器 (shim),每个 shim 运行时有 runc 作为内核源语接口,与运行容器进行交互。
-
Runc: OCI 容器的运行时规范的参考实现。用于创建容器。OCI:运行时标准、容器镜像标准。
-
containerd: 进行容器的生命周期管理——start|stop|pause|rm...。现在也可以用于管理镜像,例如拉取、推送,镜像数据/容器数据的存储。
-
根据镜像启动容器:
docker container run --name ctrl -it alpine:latest sh -
基于 alpine:latest 创建一个名为 ctrl 的容器,并进入 shell 环境。
-
cli 向 docker 守护进程接收指令,指示在 containerd 启动新的容器。
-
containerd 向 runc 传递 OCI 镜像并指示 runc 创建容器并启动 shell 环境。
-
创建容器
-
shim:与 daemon 守护解绑,实现没有 daemon 的容器。当创建新容器后,fork 出来的 runc 进程会退出。随后 containerd-shim 进程将成为容器的父进程。shim 将:
-
保持 stdin 和 stdout 开启。daemon 重启时容器不会因为管道关闭而终止。
-
退出状态反馈给 daemon。
Docker 镜像
-
镜像是一个只读的模板,独立的文件系统,(很像一个停止运行的容器),包括运行容器所需的数据,可以用来创建新的容器。
-
容器从镜像启动后,两者之间就变成了互相依赖的关系。在镜像上启动的容器全部停止之前,镜像是无法删除的。
-
镜像仓库:docker 镜像储存在镜像仓库服务中。一个仓库中可以拥有多个镜像。
-
镜像的命名和标签:采用
:分离。我们可以通过docker image pull <repo>:<tag>来拉取镜像。-a拉取仓库中所有的镜像。 -
一个镜像可以拥有多个标签。
-
filter可以用来过滤。 -
镜像由几个只读的平行层组成。通过
docker inspect可以了解这些层之间的关系。修改或创建新的内容时,将会在这些镜像层之上,创建新的镜像层。通过存储引擎(现在是快照)的方式实现堆栈,对外展示成一个统一的文件系统。 -
可以通过多架构镜像来适配当前运行环境。
--platform指定拉取对应架构镜像,也可以指定运行什么架构的容器。 -
docker image rm <ID>删除对应 ID 的镜像。同时,docker image rm $(docker image ls -q) -f删除所有本地系统内的镜像。
Docker 容器
-
镜像的运行时实例。通过
docker container run <image> <app>运行镜像中的某个应用。-it将当前终端连接到容器的对应终端上,也就是不在幕后运行镜像。 -
容器 vs 虚拟机
-
相同点:都依赖于宿主机,可以是 notebook,可以是物理服务器,也可以是公有云的一个实例。
-
虚拟机:启动后将全部物理资源通过 hypervisor 全部占有,hypervisor 通过将所有的物理资源划分成虚拟资源打包进入 VM 的软件结构中,这样用户可以使用这些虚拟机。虚拟化的是硬件资源。
-
容器:启动后会唤起所选择的操作系统。OS 占有全部的物理资源,在 OS 之上是 docker 引擎,获取 OS 的资源,例如进程树,文件系统和网络栈。接着将资源分割成相互隔离的结构,成为容器。因此虚拟化的是操作系统。

-
运行容器内的应用(例如运行 bash)后,一旦杀死该应用,容器也会退出、终止。因为这是这个容器的主进程。
-
退出容器但是并没有杀死主进程时,容器仍然在运行。此时不能通过 docker run,而是通过
docker exec -it <ID> <app>再次连接到进程。 -
docker container stop终止容器,docker container rm删除容器。数据在容器删除之前将不会被丢弃。
应用容器化
-
编写应用代码
-
dockerfile 创建,包含应用描述,依赖,以及如何运行应用。
-
对 dockerfile 进行
docker image build。 -
docker 将应用程序构建到 docker 镜像中。
对应的一些指令
-
from: 指定基础镜像作为基础镜像层。
-
label: 自定义的标签,是一组 KV 对。
-
run: 执行基础镜像层的应用,可能会安装新的应用,也是一个镜像层。
-
copy: 选取文件复制到当前镜像中,并且新建一个镜像层来存储。
-
EXPOSE: 暴露端口。
-
ENTRYPOINT: 指定入口程序。不新增镜像层。
-
docker image build -t <image>:<tag>. -
增加新的 tag 用于推送:需要
-
仓库服务,仓库,镜像标签。
-
docker image tag <image>:<tag> <repo>/<image>:<tag> -
docker image push推送到上游。
docker compose
-
单引擎进行多容器应用的部署与管理。
-
compose 文件可以使用 yaml 或者 json(前者是后者的子集)来编写。默认为
docker-compose.yml。 -
docker-compose up启动应用。 -
文件包括:version 版本,service 服务,networks 创建新的网络,volumes 创建新的卷。
docker swarm
-
集群管理。一个 swarm 由一个或多个 docker 节点构成。这些节点可以是服务器,虚拟机,树莓派或者云实例。要求通过可靠网络连接。
-
节点将会被配置成管理节点 (Manager) 或者工作节点 (worker)。
-
管理节点:负责控制平面,监控集群状态,分发工作任务,管理工作节点。
-
工作节点接受来自管理节点的任务并执行。
k8s(kubernetes)
-
开源的容器集群管理,提供集群的自动部署,扩缩容,维护等功能。分为管理节点和工作节点,类似于 docker swarm。
-
通过 CI/CD(持续集成/持续交付) 自动化流程,保证环境一致性 (CI),并确保集群可以随时部署 (CD).
组件
-
etcd: 集群状态。
-
apiserver: 提供资源操作的唯一入口,提供认证,授权,访问控制,API 注册,发现等机制。
-
controller manager: 集群状态维护。
-
scheduler: 资源调度,根据预定调度策略将 pod 调度到相应的机器上。
-
kubelet: 负责维护容器的生命周期,同时也负责卷和网络的管理。
-
runtime: 镜像管理,pod 和容器的真正运行。
-
kube-proxy: service 提供 cluster 内部的服务发现和负载均衡。
一些概念
-
Pod:最小的可部署单元。包含一个或者多个紧密耦合的容器。Pod 的主要作用是提供一个环境,可以让容器共享网络和存储资源。并且提供了容器间通信、生命周期管理等功能。
-
Deployment:部署无状态应用程序。可以随时获知当前 Pod 的部署进度。
-
service: 定义了 Pod 的逻辑集合和访问该集合的策略,是真实服务的抽象。提供了一个统一的服务访问入口和代理服务发现机制,关联多个相同 label 的 pod。
-
volume: 卷宗,一个可以被多个容器共同访问的共享目录,定义在 pod 上,可以被一个或多个 pod 中的容器挂载到某个目录下。
-
namespace: Multi-tenant 多租户隔离,将集群内部资源对象分配到不同的 namespace 中,形成逻辑上不同的小项目,小组,用户组,便于共享集群资源的同时还能被分别管理,精细化管理的粒度。
-
1Ingress:通过定义规则来管理从外部访问集群内 Service 的流量。
アイラちゃん 已收藏 (?)
面试超级短(35min),之前抱得佛脚一点没用上,总结如下。(请大家不要随便传播谢谢)
总结
中国电信:AI infra 实习
没有手撕算法部分。(国企这么好的吗)
O、根据简历自我介绍
一、拷打项目:
- 操作系统项目的简单介绍?
- 页表是怎么配置的?(内核启动时配置高页表,随后配置低地址页表,随后启动用户态程序)
- 介绍一下 linux 如何管理内存。(伙伴系统和 slab 分配器)
3.1 两者的区别是什么?(slab 分配器将内存块作为对象进行管理,进行小内存的分配,较大内存的分配通过页表进行。管理对象的生命周期和分配,这样不会在使用完内存后真正释放内存,降低时延和开销。伙伴系统是真正进行内存管理的工具,减少外部内存碎片,通过 2^n 进行分配防止内存碎片。)
3.2 slab 分配器的管理数据结构?(红黑树)。 - IPC 通信方式你选择了什么?(共享内存,这样每个进程访问相同的物理内存,提高进程通信效率)
4.1 每个进程是怎样访问相同的物理内存的?(通过自身的虚拟地址空间,以及自身的页表通过 TLB 的方式来进行地址翻译成为物理内存进行访问)。
小结:操作系统问的不深。没有问到特别高深的技术(写时拷贝,锁,时钟中断 IRQ 等)
二、拷打项目:
- 介绍以下你的阵列是如何进行加速的?
(脉动阵列 (systolic array),专门为矩阵运算进行翻译和加速。矩阵运算取的是两个矩阵的一行、一列进行乘累加操作,因此可以让矩阵的一行自左向右进入乘累加单元,让另一个数据自上而下流入乘累加单元,最后进行操作,中间值存储在寄存器之中。) - 对于大的矩阵乘法应该怎么做?(分块,计算出分块矩阵值后进行存储)
- 对于大型矩阵(分块矩阵)你的项目是否做到了 Cache 的优化?你提到了分块矩阵的存储(很遗憾没有,不过可以参考 flash attention 的方式,进行条块化和重计算后通过 HBM 优化)
建议:项目还要进行优化和深化。
三、深度学习、机器学习系统
- 你了解 Megatron 吗?(是一个大规模 LLM 训练系统,通过结合数据并行,张量并行,流水线并行的 PTP 混合并行方式来进行的系统。)
- 介绍一下 Megatron v1,v2 和 v3 是怎么进行优化的。
megatron v1 主要提出了张量并行的方式来优化 GPU 的内存不足问题。某些模型的权重 + 中间激活值太大无法储存在 GPU 中,因此将他们进行分割,分别储存在不同的节点当中。这样就可以减少不同节点之间的内存不足问题。
缺陷是没有考虑到 TP 所带来的大量通信开销问题。
2.1 向量并行一般有哪些方式?(行并行,列并行),为什么采取后者?(具有非线性激活,无法通过简单的线性防止进行分块矩阵计算,因此需要进行列并行处理)
megatron v2 采用了张量并行,数据并行和流水线并行的方式进行。
2.2 流水线并行进行了什么优化?(1F1B 交叠方式进行,这样显著减少了运算之间流水线的气泡占比)。
2.3 工作节点数目和数据并行,张量并行和流水线并行的关系?(右边三个东西的乘积就是所需的工作节点数)
2.4 microbatch 太大了有什么问题?太小了有什么问题?(microbatch 太大会导致单个节点计算缓慢,增加流水线周期。太小会导致频繁地通信,通信开销增大导致严重的通信开销)
megatron v3 采用激活重计算。因为我没有时间读完因此直接说了面试官也就没有拷打这一部分 QAQ
四、介绍以下目前的慢节点项目(组里)
- 检测到慢节点后如何恢复?(搬出了 Falcon 提出的解决办法,4 层级恢复:1.继续观察性能 2.数据重新划分 3.并行拓扑重新划分 4.检查点重启)
- 怎样才能够进行重新调度?你们有了具体方案吗?(诚实回答目前还在调研中,雏形的想法是通过 kv 的方式简化保存的内容,随后再从检查点中重新恢复。)
- 对于链路的检测和缓解,你认为应该采取什么方案?(对于拥塞,部署在网络层的全局拥塞感知已经在 Sigcomm14 上提出。通过 clos 两层架构,在叶子结点间传递相对应的拥塞指标进行。第一个是分布式架构才可以正确进行链路调度否则会出现问题(举例说明了 ECMP 性能降级),第二个是网络层可以避免绕开内核栈的问题,降低应用层复杂度)
- 目前你们将采用怎样的方式进行?(RDMA 传递,绕开内核直接写入内存)
五、有什么想问的问题?(公司目前的研发目标?有自研 teletron(感觉是 megatron 套皮没有什么新的技术在里面),也在多个层级上有进行研究)
六、评价一下自己?(目前自己的知识还比较碎片化,觉得系统的研究需要有完整的成体系的知识,以及更加深刻的了解,此外对于分布式的知识掌握仍然在入门,需要更加细致地了解相关知识)。
下午要把 leetcode 刷了,一日无 coding 则手生,两日无 coding 则手废矣,等到天晴了再出去玩一天 😋
下周二二面,啊~~~~好累
先更猫猫,回去洗完澡写完最后一题再更

2025 年 6 月 13 日 星期五 天气:小雨
上午抱佛脚➕面试 https://xjtu.app/t/topic/14653/119
中午摸鱼 1 个半小时 🥺
下午学长又要求开会,给我们安排了新的任务,不想再看论文 + 想负载均衡算法了,累
晚上做题非常不顺手,首先每日一题就给我暴击
二分 + 贪心,这种题目就是专门增加调试时间 + 卡贪心考虑不足的 QAQ
C++
class Solution {
public:
int minimizeMax(vector<int>& nums, int p) {
sort(nums.begin(), nums.end());
auto check = [&](int mx) -> bool {
int cnt = 0;
for(int i = 0; i < nums.size() - 1; i++) {
if(nums[i+1]-nums[i] <= mx) {
cnt++;
i++;
}
}
return cnt >= p;
};
int left = 0, right = nums.back() - nums[0];
while (left < right) {
int mid = (left + right) >> 1;
if (check(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
};
然后战一些很奇怪的网格图 dijkstra。
这题可以用并查集,也可以用 dijkstra。并查集的思路就像水逐渐淹过去一样,建立一个高度 -> 索引的映射即可,这里直接用 vector 就好。直到某个时刻起点和终点相互连接。
C++
class Union {
public:
vector<int> parents;
vector<int> size;
int n;
Union(int n):n(n), parents(vector<int>(n)), size(vector<int>(n, 1)) {
iota(parents.begin(), parents.end(), 0);
}
int find(int id) {
if(parents[id] != id) {
return find(parents[id]);
}
return id;
}
void merge(int id1, int id2) {
int pa1, pa2;
pa1 = find(id1), pa2 = find(id2);
if(pa1 == pa2) return;
else if(size[pa1] < size[pa2]) {
parents[pa1] = pa2;
size[pa2] += size[pa1];
} else {
parents[pa2] = pa1;
size[pa1] += size[pa2];
}
}
};
class Solution {
public:
int swimInWater(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<int> indices(m*n); // Tips: No repeat.
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
indices[grid[i][j]] = i*m+j;
}
}
constexpr int dir[4][2] = {{-1, 0},{1, 0},{0, -1},{0, 1}};
Union u(m*n);
// Merge.
int t;
for(t = 0; t < m*n; t++) {
int cur = indices[t];
int cur_i, cur_j;
cur_i = cur / m, cur_j = cur % m;
for(int i = 0; i < 4; i++) {
int new_i = cur_i + dir[i][0], new_j = cur_j + dir[i][1];
if(new_i >= 0 && new_i < m && new_j >= 0 && new_j < n) {
int adj_height = grid[new_i][new_j];
if(adj_height <= t) {
u.merge(cur, new_i * m + new_j);
}
}
}
if(u.find(0) == u.find(m*n - 1)) {
break;
}
}
return t;
}
};
dijkstra 算法就是网格图,不需要建图(会导致非常稠密浪费时间和空间)。
C++
// The Dijkstra algorithm.
constexpr int directions[4][2] = {{-1, 0},{1, 0},{0, -1},{0, 1}};
class Solution {
public:
int swimInWater(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
int N = m * n;
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
vector<int> distance(N, INT_MAX);
distance[0] = grid[0][0];
pq.emplace(distance[0],0);
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(dist > distance[cur]) continue;
// Coordinate decomposition. cur = i * m + j.
int cur_i = cur / m, cur_j = cur % m;
for(int i = 0; i < 4; i++) {
int adj_i = cur_i + directions[i][0];
int adj_j = cur_j + directions[i][1];
int adj = adj_i * m + adj_j;
if(adj_i >= 0 && adj_i < m && adj_j >= 0 && adj_j < n) {
int new_dist = max(dist, grid[adj_i][adj_j]);
if(distance[adj] > new_dist) {
distance[adj] = new_dist;
pq.emplace(distance[adj], adj);
}
}
}
}
return distance[N - 1];
}
};
目前还想不出来,明天再战(对不起 ldx 想睡觉 😭)
主要的想法就是这题目比较复杂,我们需要了解到,这样一个稠密图我们只需要计算需要进行计算的邻边。我们的目标是 start -> target,因此第一条邻边就是两者之间的曼哈屯距离~
接下来需要考虑的是特殊边。我们看下图就明白应该执行怎样的“松弛操作”(算法导论中的术语)
主要是不能晕掉,牢靠把握 dijkstra 的流程:
寻找起点的邻边 -> 进行松弛操作 -> 更新则加入队列 -> 对新的邻边进行松弛操作 -> 直到终点为止。图中就是分别对 j 进行了邻边 i 的松弛操作,对 n-1 进行了 j 的松弛操作。
然后就是防止内存爆炸 需要对 进行编码操作。这里编码采用 (x << 32)|y ,解码采用 x = key >> 32, y = key & ((1ll << 32) - 1) 的方式进行。

C++
using ll = long long;
constexpr ll MASK = (1ll << 32) - 1;
class Solution {
public:
ll coord2Key(int i, int j) {
ll key = ((ll)i << 32) | j;
return key;
}
pair<int,int> key2Coord(ll key) {
int i = key >> 32;
int j = key & MASK;
return make_pair(i, j);
}
int minimumCost(vector<int>& start, vector<int>& target, vector<vector<int>>& specialRoads) {
priority_queue<pair<ll,ll>, vector<pair<ll,ll>>, greater<pair<ll,ll>>> pq;
unordered_set<ll> visited;
unordered_map<ll, ll> distance;
int start_x = start[0], start_y = start[1];
int target_x = target[0], target_y = target[1];
ll s = coord2Key(start_x, start_y), t = coord2Key(target_x, target_y);
distance[s] = 0;
pq.emplace(0, s);
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(cur == t) break;
if(visited.find(cur) != visited.end()) continue;
visited.insert(cur);
// Update current point to target.
const auto & [cur_x, cur_y] = key2Coord(cur);
if(distance.find(t) == distance.end() || distance[t] > dist + llabs(cur_x - target_x) + llabs(cur_y - target_y)) {
distance[t] = dist + llabs(cur_x - target_x) + llabs(cur_y - target_y);
pq.emplace(distance[t], t);
}
// Update the special roads' ends. start -> cur --- (sp.road) ---> end;
for(const auto & roads: specialRoads) {
ll new_dist = dist + (ll)abs(roads[0] - cur_x) + (ll)abs(roads[1] - cur_y) + roads[4];
ll adj = coord2Key(roads[2], roads[3]);
if(distance.find(adj) == distance.end() || new_dist < distance[adj]) {
distance[adj] = new_dist;
pq.emplace(distance[adj],adj);
}
}
}
return distance[t];
}
};
祝各位晚安~
写累了,稍微总结一下今天写的题目
乍一看没反应过来这怎么用 dijkstra 做,稍微看了看提示,原来是当 成立的时候,将边权设立为 即可。
判断质数采用埃氏筛即可。这一部分预处理完成。
C++
constexpr int MAXN = 1e4+5;
bool notPrime[MAXN]; // When not prime, mark true.
// Eratosthenes prime number calculation.
int init = []() -> int {
notPrime[0] = true, notPrime[1] = true;
for(int i = 2; i < MAXN; i++) {
if(notPrime[i] == false) {
for(int j = i * i; j < MAXN; j += i) {
notPrime[j] = true;
}
}
}
return 0;
}(); // found all prime number.
auto length = [](int num) -> int {
int len = 0;
while(num) {
num /= 10;
len++;
}
return len;
};
class Solution {
public:
int minOperations(int n, int m) {
if(notPrime[n] == false || notPrime[m] == false) {
return -1;
}
int len_n = length(n);
vector<int> distance(pow(10, len_n), INT_MAX);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
pq.emplace(n, n);
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(cur == m) return dist;
if(dist > distance[cur]) continue;
int ratio = 1;
for(int digit = cur; digit > 0; digit /= 10) {
if(digit % 10 < 9) {
int adj = cur + ratio;
if(notPrime[adj] && distance[adj] > dist + adj) {
distance[adj] = dist + adj;
pq.emplace(distance[adj],adj);
}
}
if(digit % 10 > 0) {
int adj = cur - ratio;
if(notPrime[adj] && distance[adj] > dist + adj) {
distance[adj] = dist + adj;
pq.emplace(distance[adj],adj);
}
}
ratio *= 10;
}
}
return -1;
}
};
换一些思路做一下 dp
这样我们的转移就是在 上倒序进行的,并且可以优化成两个一维数组。
C++
class Solution {
public:
int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
int m = grid.size();
int n = grid[0].size();
// vector<vector<int>> dp = vector<vector<int>>(m, vector<int>(n,0));
int * dp_old = new int [n];
int * dp_new = new int [n];
for(int j = 0; j < n; j++) {
dp_old[j]= grid[0][j];
dp_new[j] = dp_old[j];
}
int ans = INT_MAX / 2;
for(int i = 1; i < m; i++) {
for(int j = 0; j < n; j++) {
int temp = INT_MAX / 2;
for(int k = 0; k < n; k++) {
temp = min(temp, dp_old[k]+moveCost[grid[i-1][k]][j]);
}
dp_new[j] = temp + grid[i][j];
if(i == m-1) ans = min(ans, dp_new[j]);
}
int * temp = dp_old;
dp_old = dp_new;
dp_new = temp;
}
return ans;
}
};
这一题采用记忆化的 dfs 方法来进行。因为我们有重复的分支,因此将其记录下来防止指数级别增长即可。
C++
constexpr int directions[4][2] = {{1,0},{-1,0},{0,1},{0,-1}}; // Directions in four ways, RLUD.
class Solution {
vector<vector<int>> memo;
int m;
int n;
public:
int dfs(int x, int y, vector<vector<int>> & mat) {
if(memo[x][y] != -1) return memo[x][y]; // Prune the dfs branch.
memo[x][y] = 1; // Initial value with 1.
for(int i = 0; i < 4; i++) {
int nx = x + directions[i][0], ny = y + directions[i][1];
if(nx >= 0 && nx < m
&& ny >= 0 && ny < n
&& mat[nx][ny] > mat[x][y]) {
memo[x][y] = max(dfs(nx, ny, mat) + 1, memo[x][y]);
}
}
return memo[x][y];
}
int longestIncreasingPath(vector<vector<int>>& matrix) {
m = matrix.size();
n = matrix[0].size();
// Abnormal situation dealing with m,n has 0.
if(m == 0 || n == 0) {
return 0;
}
memo = vector<vector<int>>(m, vector<int>(n,-1));
int ans = INT_MIN / 2;
// DFS started at each coord.
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
ans = max(dfs(i,j,matrix),ans);
}
}
return ans;
}
};
思考:dijkstra 与 dp 的区别?
一般来说在图上进行 dp 的时候需要保证无后效型。即后面更新的动作无法影响到已经更新完成的部分。而 dijkstra 算法具有松弛操作,会根据后面点的更新而影响前者(因为后面更新的节点会加入到队列中,从而回过头来可能更新原先遍历过的点)。因此需要观察是否更新无后效型。
今天好热啊下午又闷又热让アイラちゃん做题进展缓慢 😵💫
不学了今晚摸鱼 🐟️
周末要休息一下 😋
double ![]()

アイラちゃん 进行一个赛博吸猫 🫳
アイラちゃん 好乖的小猫🐱
神秘小维 不知道为什么我看这两小只很像两块砖头🧱 🤣
猫猫砖可爱捏

像这个⬆️
小结一下然后学一下数学~
暴力即可。
C++
class Solution {
public:
int maxDiff(int num) {
string s = to_string(num);
string original = s;
int minNum = INT_MAX, maxNum = INT_MIN;
for(int x = 0; x <= 9; x++) {
for(int y = 0; y <= 9; y++) {
for(int i = 0; i < s.length(); i++) {
if(s[i] == '0' + x) {
s[i] = '0' + y;
}
}
if(s[0] != '0' && stoi(s) != 0) {
minNum = min(stoi(s), minNum);
maxNum = max(stoi(s), maxNum);
}
s = original;
}
}
return maxNum - minNum;
}
};
对于 Dijkstra 进行更深一步的考察。对于最短路我们每次都需要进行一次松弛操作,于是对于那些比最短路更长一点的第二短路径也进行松弛操作即可。需要注意的是这里的边是无权边(具有相同权重的边也可以视为无权边),就可以采用普通的 queue 进行松弛操作。
C++
// Using BFS to find the shortest path with the second shortest path.
class Solution {
public:
int secondMinimum(int n, vector<vector<int>>& edges, int time, int change) {
// Step 1: Build graph here.
vector<vector<int>> graph(n+1);
for(const auto edge: edges) {
int u = edge[0], v = edge[1];
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
// Step 2: function that get the next time.
auto step = [&](int curTime) -> int {
int round = curTime / change; // Whether it is green or not. % 2 == 0: green;
if(round % 2 == 1) {
return (round + 1) * change + time; // next green light + edge weight.
}
return curTime + time;
};
// Step 3: BFS for 0-1 shortest path.
vector<vector<int>> distance(2, vector<int>(n+1, INT_MAX)); // distance[0] -> shortest;
queue<pair<int,int>> q; // 0-1 BFS using queue is OK;
q.emplace(0, 1);
distance[0][1] = 0;
while(!q.empty()) {
const auto [dist, cur] = q.front();
q.pop();
for(const auto & adj: graph[cur]) {
int next_time = step(dist); // Calculate new time.
if(distance[0][adj] > next_time) {
distance[0][adj] = next_time;
q.emplace(next_time, adj);
} else if(distance[0][adj] < next_time && distance[1][adj] > next_time) {
distance[1][adj] = next_time;
q.emplace(next_time, adj);
}
}
}
return distance[1][n];
}
};
看到最大中找最小其实第一反应是二分。于是可以进行 dfs 判断访问其他点,从而获得最大值,二分获得目标之下的最大值。但是此时假设边权最大为 ,如果边权很大将会被卡掉时间复杂度 。我们可以看到边权最大值是 , 但是反向建图+dijkstra 的时间复杂度是 , 所以我们选择 dijkstra 即可。
只需要计算出来所有距离的最大值即可。
C++
using pii = pair<int,int>;
class Solution {
public:
int minMaxWeight(int n, vector<vector<int>>& edges, int threshold) {
if (edges.size() < n - 1) {
return -1;
}
vector<vector<pii>> graph(n);
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], w = edge[2];
graph[v].emplace_back(u, w);
}
priority_queue<pii, vector<pii>, greater<pii>> pq;
vector<int> distance(n, INT_MAX);
pq.emplace(0, 0);
distance[0] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(dist > distance[cur]) continue;
for(const auto & [adj, w]: graph[cur]) {
if(distance[adj] > max(dist, w)) {
distance[adj] = max(dist, w);
pq.emplace(distance[adj], adj);
}
}
}
int ans = *max_element(distance.begin(), distance.end());
return ans == INT_MAX ? -1 : ans;
}
};
有点难。具体的做法还是比较容易想的,实际写起来会有一点问题。
- 首先还是套 dijkstra。
- 接着统计一下在限度内能够访问到的真正的节点。
- 最后统计超过阈值的边有多少能够访问。
- 计算{u,v}的访问情况。假设有 dist[u],dist[v], 所以左边的点能够继续访问 maxMove - dist[u], 右边的点能够访问 maxMove-dist[v]。很明显要保证非负数。
- 最后将两者相加,取边分裂节点和他的最小值即可。
C++
using pii = pair<int,int>;
class Solution {
public:
int reachableNodes(vector<vector<int>>& edges, int maxMoves, int n) {
vector<vector<pii>> graph(n);
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], cost = edge[2] + 1; // move will be more than 1.
graph[u].emplace_back(v, cost);
graph[v].emplace_back(u, cost);
}
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.emplace(0, 0);
vector<int> distance(n, INT_MAX);
distance[0] = 0;
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(distance[cur] < dist) continue;
for(const auto & [adj, w]: graph[cur]) {
if(distance[adj] > dist + w) {
distance[adj] = dist + w;
pq.emplace(dist + w, adj);
}
}
}
int ans = 0;
for(const auto & dist: distance) {
if(dist <= maxMoves) ans++; // Calculate two endpoints.
}
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], w = edge[2];
int u_dist = max(0, maxMoves - distance[u]);
int v_dist = max(0, maxMoves - distance[v]);
ans += min(u_dist + v_dist, w);
}
return ans;
}
};
明天打算开始不定期学一下 golang 了
要兑现自己说过的话 😇
如果有能力也想做一些 lab

好不容易晴朗一次,恢复一下


アイラちゃん p3 是哪里啊
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 图书馆——(旧)电院群楼的路上的桥上
アイラちゃん (旧)电院
悲😭
回来吧我的电院()
长恨此身非我有 何时忘却营营
2025 年 6 月 16 日 星期一 天气:多云/晴
X: @AlicitruSalt

今天是难得一见的好天气。所以上午就被要求写文档了。
先和学长再次确定了他们需要什么,面对节点故障,链路故障和多任务的情况下 ECMP 在两层 clos 数据中心中会怎么表现从而出现拥塞的情况。一上午就是画图/讨论/思考就结束了。
下午正式解决代码库的问题。我们面临的问题如下:
- 基于离散事件网络仿真工具 ns-3.18 开发。
- 当前代码采用 gcc-5/g++-5 才能运行,高于该版本的将会出错。而我也没有余力进行重构,这是一个痛苦的工作。
- 代码采用经典 makefile 模式,需要构造
compile_commands.json来支持 language server(这里采用 clangd)。 - vscode debug 无法进行。
于是首先:
我们寻找到了 ubuntu 18.04 server 版虚拟机。鉴于我的电脑是 M1 芯片,因此选择 aarch64/arm64 版本安装在 UTM 上。(docker 太慢了并且远程调试目前无法解决,太菜 QAQ)
随后,UTM 的屏幕显示 No display available。无法通过屏幕进行任何操作,这是因为上古的 ubuntu 的 grub 引导程序不会从屏幕输出,需要我们新增加一个串行端口。新建一个串行端口即可。

随后我们只安装 openssh 服务器和必需的服务即可。这样就启动了。然后安装 gcc/g++ 5.5,gdb, make, bear,clangd-8 并且在 /usr/bin 创建软连接指向 gcc/g++/gdb/clangd即可。
随后采用 bear make 的方式来构建我们的项目,bear 将会自动追踪 makefile 过程生成我们需要的 compile_commands.json。将其作为 clangd 的参考文件提供给 language server 即可。
最后解决 debug 问题。我们构建文件后,只需要找到目标运行文件,将其作为 launch.json 的 program 的值即可。但是此时就会爆出错误:XXX_debug.so src file doesn't exist。因此我们需要添加一些动态链接库,也就是给我们的 LD_LIBRARY_PATH. 我们只需要提供给环境系统 lib 和项目构建出来的 lib 即可。我们添加上:
{
"environment": [
{
"name": "PATH",
"value": "${workspaceFolder}/build/lib:${env:PATH}"
}
]
}
这样 debug 也就成功完成了。
晚上自然还是刷 leetcode 的。今天复习 floyd 算法后,明天就可以进入 bellman-ford 算法,prim-kruskal 算法的复习了。待图论大部分结束后,打算再转入 dp 和二分的复习。
前缀最小优化 O(n)。 当然可以。
C++
class Solution {
public:
int maximumDifference(vector<int>& nums) {
vector<int> prefix_min(nums.size(), INT_MAX);
prefix_min[0] = nums[0];
for(int i = 1; i < nums.size(); i++) {
prefix_min[i] = min(prefix_min[i-1], nums[i]);
}
int max_diff = -1;
for(int i = 1; i < nums.size(); i++) {
if(nums[i] == prefix_min[i-1]) continue;
max_diff = max(nums[i]-prefix_min[i-1], max_diff);
}
return max_diff;
}
};
Floyd 算法:最关键的就是遍历每个节点,随后判断将节点 k 塞入 u -> v 的道路中是否会小于当前值。
for k = 0; k < n; k++ {
for i = 0; i < n; i++ {
for j = 0; j < n; j++ {
f[i][j] = min(f[i][j], f[i][k]+f[k][j]);
}
}
}
C++
class Solution {
public:
int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
vector<vector<int>> f(n, vector<int>(n, INT_MAX));
for(int i = 0; i < n; i++) {
f[i][i] = 0;
}
for(const auto & edge: edges) {
int u = edge[0], v = edge[1], w = edge[2];
f[u][v] = w;
f[v][u] = w;
}
for(int k = 0; k < n; k++) {
for(int u = 0; u < n; u++) {
if(f[u][k] == INT_MAX) continue; // Avoid overflow;
for(int v = 0; v < n; v++) {
if(f[k][v] == INT_MAX) continue;
f[u][v] = min(f[u][v], f[u][k] + f[k][v]);
}
}
}
vector<int> candidate(n, 0);
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
if(f[i][j] <= distanceThreshold) {
candidate[i]++;
}
}
}
int min_index = 0, min_city = INT_MAX;
for(int i = 0; i < n; i++) {
if(candidate[i] <= min_city) {
min_index = i;
min_city = candidate[i];
}
// cout << candidate[i] << ' ';
}
return min_index;
}
};
尝试一下静态数组吧。
C++
class Solution {
public:
long long minimumCost(string source, string target, vector<char>& original, vector<char>& changed, vector<int>& cost) {
using ll = long long;
ll f[26][26];
int n = 26;
for(int i = 0; i < n; i++) {
memset(f[i], 0x7f, sizeof(ll) * n);
f[i][i] = 0;
}
for(int i = 0; i < cost.size(); i++) {
ll u = original[i] - 'a', v = changed[i] - 'a', w = cost[i];
f[u][v] = min(w, f[u][v]);
}
for(int k = 0; k < n; k++) {
for(int i = 0; i < n; i++) {
if(f[i][k] == 0x7f7f7f7f7f7f7f7f) continue;
for(int j = 0; j < n; j++) {
if(f[k][j] == 0x7f7f7f7f7f7f7f7f) continue;
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
}
}
ll ans = 0;
if(source.length() != target.length()) return -1;
for(int i = 0; i < source.length(); i++) {
int src = source[i] - 'a', tgt = target[i] - 'a';
ll cost = f[src][tgt];
if(cost == 0x7f7f7f7f7f7f7f7f) return -1;
ans += cost;
}
return ans;
}
};
需要枚举子集。枚举子集的方式就是从 i=0 -> i=(1 << n), 判断某一个元素是否被取到可以有:i & (1 << k) 或者 (i >> k) & 1。
时间复杂度枚举 个子集,并做一次 floyd . 也就是 . Floyd 算法需要 空间。
C++
class Solution {
public:
int numberOfSets(int n, int maxDistance, vector<vector<int>>& roads) {
int **f = new int * [n];
int **graph = new int * [n];
// iterate over the subset.
for(int i = 0; i < n; i++) {
graph[i] = new int [n];
f[i] = new int [n];
memset(graph[i], 0x7f, sizeof(int) * n);
memset(f[i], 0x7f, sizeof(int) * n);
graph[i][i] = 0;
f[i][i] = 0;
}
for(const auto & edge: roads) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u][v] = min(w, graph[u][v]);
graph[v][u] = min(w, graph[v][u]);
}
int ans = 0;
for(int subset = 0; subset < (1 << n); subset++) {
// copy the viable f.
for(int i = 0; i < n; i++) {
if((subset >> i) & 1) {
for(int j = 0; j < n; j++) {
f[i][j] = graph[i][j];
}
}
}
for(int k = 0; k < n; k++) {
if(((subset >> k) & 1) == 0) continue;
for(int u = 0; u < n; u++) {
if(f[u][k] == 0x7f7f7f7f || ((subset >> u) & 1) == 0) continue;
for(int v = 0; v < n; v++) {
if(f[k][v] == 0x7f7f7f7f) continue;
f[u][v] = min(f[u][v], f[u][k] + f[k][v]);
}
}
}
bool flag = true;
for (int i = 0; i < n; i++) {
if (((subset >> i) & 1) == 0) continue;
for (int j = 0; j < i; j++) {
if ((subset >> j) & 1 && f[i][j] > maxDistance) {
flag = false;
break;
}
}
if(!flag) break;
}
if(!flag) continue;
ans += 1;
}
return ans;
}
};
今天就是这样了,又没有看 golang,该罚!!!, 大家晚安好梦~,明天上午 11 点二面,到时候再放一下面筋吧。
今天有猫猫 ![]()

アイラちゃん 猫猫怎么每天都在揣手手
アイラちゃん 不懂就问,这是猫猫固定刷新点吗 ![]()
对的,固定点蹲草就好了
神秘小维
アイラちゃん 太形象了 🤣
二面面筋,感觉上了一点难度 (40 min),无手撕算法
实习生现在都这么难,到校招/社招岂不是炸了哥们 😵💫
总结
一、拷打项目(操作系统)
- 多核处理部分:时钟中断如何进行的?(特殊寄存器将状态保存到系统寄存器中,随后写入内存,随后下陷到异常向量表处理时钟中断,再反过来进行恢复)。
- 如果中断过程中下电应该如何恢复?(不会,没学过,寄)
- 多进程通信方式有哪些?(管道,消息队列,共享内存,信号量)
- 共享内存通信需要注意什么问题?(保证 ACID 性。)你的项目里通过什么方式来保证?(增加读写锁)。读写锁主要有哪两种?(共享锁(读锁),互斥锁(写锁))。读写锁是否一定能够保证共享内存部分维持 ACID 性?(不能,要遵循两阶段锁方式)。
聊着聊着变成数据库了,感觉有点小崩 - 有关虚拟文件系统,你是否了解 NvMe?(只是听说过,具体不懂)
二、拷打项目(玻尔兹曼机)
- 介绍一下这是个什么?(介绍这是一个很古老的模型,借用了物理模型中收敛态能量越低的方式来训练网络成为生成式的模型)
- 你了解最近比较流行的生成式模型吗?(不了解,介绍了高斯 splatting 由 2d 重建 3d 模型,VAE(变分自编码),以及 diffusion 模型,但是把 diffusion 和 VAE 搞混了,被告知变分自编码没有扩散,完蛋)
三、拷打 RDMA
- 简要介绍一下 RDMA。(借用了直接内存访问 DMA 的方式,让数据绕过内核直接通过网络写入到目标的内存中,防止特权级(异常)切换,提升速度应对 AI 场景)
- 有什么特性?(想了两个:零拷贝技术:不需要将数据
从网卡拷贝到 cpu,这里被告知有点小问题,应该是从内核拷贝到用户空间,以及内核旁路技术,绕开内核由网卡直接给应用写入数据)。 - 你认识到的有哪些协议?(RoCE,iWarp,IB)
- 了解 RDMA 单边/双边操作吗?(有点晕,刚开始根本没想到这是什么,后来经过提醒记起来了。双边操作是 send-receive,单边操作是 write-read。)
- 哪些可靠性保护?(只记得 ACK,忘记还有 CRC 校验了,计网谁记得啊)
到这里面试官估计再问深下去就崩了,所以就不问了
四、框架/数据中心
- 前面介绍到了 RDMA 在数据中心内的应用,你认为还有什么需要优化的地方?(介绍了目前 TCP 流的控制,流的间隔是微秒级别,而 RDMA 的模式是纳秒级别爆发流,因此现有的自适应路由,负载均衡等都需要改进进行纳秒级别的响应。)
- 了解过 PD 分离吗?(介绍了 prefill-decode 分离,前者是计算密集部分,后者是存储密集部分,除此之外就没太深想了)
- 上次你介绍了 TP+PP+DP 混合部分。你还了解其他的并行策略吗?(只知道还有一个 EP,剩下的部分 SP 等等完全不了解)
- 了解 kv cache paged attention 吗?(
我又不是做存储的你问这么难干什么我没那么牛,前几天刷微信的时候碰巧看到),就是借助了操作系统的页表,TLB 和换页的方式来进行 kv cache 的缓存和快速访问,也就是提高命中率。 - paged attetion 有什么方式可以更好地复用?(借助 OS 了那就猜 copy-on-Write 写时拷贝,TLB 建立快表查询等),实际上如何生成的你知道吗?(不了解)
四、有什么问题吗?
请问有什么资源可以了解存储相关的东西?(说会给我介绍一些论文)
总评:面试官感觉比上次专业好多,能够在网络和存储说的有来有回,也强调了目前我拥有的 RDMA 的知识略有欠缺,对于存储还是希望加强学习。不知道目前结果怎么样。
双系统安装 (Windows 10 + ubuntu 18.04)
- 下载 easy UEFI,准备好一个空的 U 盘,下载 ubuntu 18.04 amd64 iso 文件
- 通过 easy UEFI 制作好了一个启动盘。启动盘制作时,选择 U 盘作为 disk 分区,选择 EFI/bootx64.EFI 作为引导项。
- 对磁盘进行分区。对于磁盘 1,我们需要借助他的 EFI 启动分区来帮助启动我们的 ubuntu。因此我们将其划出 1GB 的分区空间作为/boot 挂载点。efi 分区将自动扫描检测得到。接着将磁盘 2 划分出 600GB 作为根目录挂载空间。
- 随后选择我们的启动盘启动,进入 grub 引导。随后按照我们划分的分区来格式化成 ext4 文件格式提供给 ubuntu18.04(注意!千万不能将整个盘作为 ubuntu 系统的启动盘,不然数据都没了不就寄了)
- 进入 ubuntu,仅安装 openssh,剩下的到时候再说吧。
ubuntu 配置
- 没有网络怎么办?先以太网连接,再配置无线网络。
- ifconfig -a 找到无线网卡,这里我们的无线网卡是 wlp4s0,也可以通过 iw dev 来显示。
- 接下来进入到 /etc/netplan 中进行编辑 00-installer-config.yaml。我们主要编辑如下:
network:
ethernets:
enp3s0:
dhcp4: true
wifis:
wlp4s0:
dhcp4: true
access-points:
"<WIFI-SSID>":
password: "password"
version: 2
最后执行:
apt install wpasupplicant
apt install network-manager
netplan generate
netplan apply
拔掉物理网线即可。
- 配置超时策略
接下来解决启动过程中等待时间过长问题,经过排查得知是强行等待网络连接直至 2min30s 超时导致,于是手动修改
systemd-networkd-wait-online.service规则。cd /etc/systemd/system/network-online.target.wants/ sudo vim systemd-networkd-wait-online.service在 [Services] 一栏添加
TimeoutSec=5,强制超时 5s 后立刻启动。
- 从疑难杂项中选择从其他镜像启动,选择 ubuntu 即可。
暑い、暑くて干からびそう、動けないよに暑いよ♪~
真的好热 🥵

アイラちゃん 
![]()
看看我们
2025 年 6 月 17 日 星期二 天气晴
今天非常非常热。
上午九点钟到大零号湾,开始配置跳板机的双系统。详情请见经验贴:https://xjtu.app/t/topic/14653/144
在被磁盘分区搞的晕乎乎的时候,11 时开始了二面,面筋可以见:https://xjtu.app/t/topic/14653/143
下午继续鼓捣跳板机的双系统,并且配置好简单的 openssh 功能,搭建工具链,配置项目,构建项目,写好 vscode 调试脚本,随后“下班”回学校跑步。
晚上复习 prim 和 kruskal 算法。
- prim 算法:从一个点开始,不断遍历连接该点的边,计算出最小距离后,将邻接点加入集合,并继续选取与该集合内点相连的最小边,最后形成完整的最小生成树。采用 distance 数组维护即可。暴力时间复杂度 . 如果是稀疏图可以通过堆来进行优化成 。
- kruskal 算法:贪心地选择符合条件的边,通过并查集判断是否合法。.
很明显这是一个稠密图。这样 prim 算法远远快于 kruskal。
Prim
class Solution {
public:
int minCostConnectPoints(vector<vector<int>>& points) {
// No building graph.
int n = points.size();
int * distance = new int [n];
int * visited = new int [n];
memset(distance, 0x7f, sizeof(int) * n);
memset(visited, 0, sizeof(int) * n);
int cur = 0, ans = 0;
for(int i = 1; i < n; i++) {
visited[cur] = 1;
int minDist = 0x7f7f7f7f;
int pre = cur;
for(int j = 0; j < n; j++) {
if(!visited[j]) {
distance[j] = min(distance[j], abs(points[pre][0] - points[j][0]) + abs(points[pre][1] - points[j][1]));
if(distance[j] < minDist) {
cur = j;
minDist = distance[j];
}
}
}
ans += minDist;
}
return ans;
}
};
kruskal
class Union {
int * parent;
int num;
int * size;
public:
Union(int n): num(n) {
parent = new int [n];
size = new int [n];
for(int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
int find(int id) {
if(parent[id] != id) {
return find(parent[id]);
}
return id;
}
void merge(int id1, int id2) {
int pa1 = find(id1);
int pa2 = find(id2);
if(pa1 == pa2) {
return;
} else if(size[pa1] > size[pa2]) {
parent[pa2] = pa1;
size[pa1] += size[pa2];
} else {
parent[pa1] = pa2;
size[pa1] += size[pa2];
}
}
bool judge(int id1, int id2) {
return find(id1) == find(id2);
}
};
struct Node {
int id1, id2;
int w;
Node(int i, int j, int w): id1(i), id2(j), w(w) {}
bool operator>(const Node &n) const {
return w > n.w;
}
};
class Solution {
public:
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size();
priority_queue<Node, vector<Node>, greater<Node>> pq;
Union u(n);
for(int i = 0; i < n; i++) {
for(int j = i + 1; j < n; j++) {
Node tmp(i,j, abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]));
pq.emplace(tmp);
}
}
int cost = 0;
while(!pq.empty()) {
Node tmp = pq.top();
pq.pop();
if(!u.judge(tmp.id1, tmp.id2)) {
u.merge(tmp.id1, tmp.id2);
cost+=tmp.w;
}
}
return cost;
}
};
1489. 找到最小生成树里的关键边和伪关键边 明天再做,太难了
竟然可以 kruskal 做 2E+1 次来判断吗,想得复杂了,还以为要用 tarjan 求桥边,tarjan 已经忘完了,模板也看不懂了。
C++
class Union {
int * parent;
int num;
int * size;
public:
Union(int n): num(n) {
parent = new int [n];
size = new int [n];
for(int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
int find(int id) {
if(parent[id] != id) {
return find(parent[id]);
}
return id;
}
void merge(int id1, int id2) {
int pa1 = find(id1);
int pa2 = find(id2);
if(pa1 == pa2) {
return;
} else if(size[pa1] > size[pa2]) {
parent[pa2] = pa1;
size[pa1] += size[pa2];
num--;
} else {
parent[pa1] = pa2;
size[pa1] += size[pa2];
num--;
}
}
bool judge(int id1, int id2) {
return find(id1) == find(id2);
}
int getNum() {
return num;
}
};
using tup = tuple<int,int,int,int>;
class Solution {
public:
vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>>& edges) {
priority_queue<tup, vector<tup>, greater<>> pq, pq_all;
Union un(n);
for(int i = 0; i < edges.size(); i++) {
int u = edges[i][0], v = edges[i][1], w = edges[i][2];
pq_all.emplace(w, i, u, v);
}
int min_val = 0;
pq = pq_all;
while(!pq.empty()) {
const auto [w, ind, u, v] = pq.top();
pq.pop();
if(!un.judge(u,v)) {
min_val += w;
un.merge(u,v);
}
}
// Find key edge/fake edge by deleting them.
vector<vector<int>> ans(2);
for (int i = 0; i < edges.size(); i++) {
// first pass -> key
Union uf(n);
int val = 0;
pq = pq_all;
while(!pq.empty()) {
const auto [w, ind, u, v] = pq.top();
pq.pop();
if(!uf.judge(u,v) && ind != i) {
val += w;
uf.merge(u,v);
}
}
if (uf.getNum() != 1 || (uf.getNum() == 1 && val > min_val)) {
ans[0].push_back(i);
continue;
}
// second pass -> fake key;
Union uf2(n);
pq = pq_all;
uf2.merge(edges[i][0], edges[i][1]);
val = edges[i][2];
while(!pq.empty()) {
const auto [w, ind, u, v] = pq.top();
pq.pop();
if(!uf2.judge(u,v) && ind != i) {
val += w;
uf2.merge(u,v);
}
}
if (val == min_val) {
ans[1].push_back(i);
}
}
return ans;
}
};
数学题。先隔开数组再添上数字。
- 计算有多少种分割方案,即从 个空隙中选择 个隔板的方案数。 。
- 第一段有多少种。既然第一段所有元素都一样,那么只看第一个数,它可以在 中任意选,所以第一段有 种。
- 后面不能与第一段相同,所以有 种,并且有 行。
组合数计算套模板吧。cv 大法启动~
C++
const int MOD = 1e9 + 7;
const int MX = 1e5;
long long fact[MX];
long long inv_fact[MX];
class Solution {
long long qpow(long long x, int n) {
long long res = 1;
while (n) {
if (n & 1) {
res = res * x % MOD;
}
x = x * x % MOD;
n >>= 1;
}
return res;
}
long long comb(int n, int m) {
return fact[n] * inv_fact[m] % MOD * inv_fact[n - m] % MOD;
}
void init() {
if (fact[0]) {
return;
}
fact[0] = 1;
for (int i = 1; i < MX; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
inv_fact[MX - 1] = qpow(fact[MX - 1], MOD - 2);
for (int i = MX - 1; i; i--) {
inv_fact[i - 1] = inv_fact[i] * i % MOD;
}
}
public:
int countGoodArrays(int n, int m, int k) {
init();
return comb(n - 1, k) * m % MOD * qpow(m - 1, n - k - 1) % MOD;
}
};
今日份猫猫 ![]()

我也有班上了, 🤑
アイラちゃん お金が欲しい🤑
お金が欲しい!🤑
元/円/$/£
アイラちゃん 人類には三大欲望がある――金を稼ぐこと、金を稼ぐこと、そして金を稼ぐことだ🤑
写 leetcode 累了,来总结一下换一下脑瓜子
排序,直接判断就好了。应该是简单题。
C++
class Solution {
public:
vector<vector<int>> divideArray(vector<int>& nums, int k) {
ranges::sort(nums);
int n = nums.size();
vector<vector<int>> ans(n/3);
for(int i = 0; i < n; i += 3) {
if(nums[i+2] - nums[i] > k) {
ans = {};
break;
}
for(int j = i; j < i + 3; j++) {
ans[i/3].push_back(nums[j]);
}
}
return ans;
}
};
一开始觉得是贪心,但是实在不知道该怎么贪心。后来画了画图,发现可以用 MST 做,就用并查集做了,结果看了数据范围并查集根本存不下。那么就只能直接合并。
每次合并都是合并一行/一列,那么这样的话每次只需要先对水平/垂直进行排序,然后挑选一行/一列合并即可。将行/列看作是连通分量就好了。(没想到可以写这么短)

C++
class Solution {
public:
long long minimumCost(int m, int n, vector<int>& horizontalCut, vector<int>& verticalCut) {
ranges::sort(horizontalCut);
ranges::sort(verticalCut);
long long ans = 0;
int i = 0, j = 0;
while (i < m - 1 || j < n - 1) {
if (j == n - 1 || i < m - 1 && horizontalCut[i] < verticalCut[j]) {
ans += horizontalCut[i++] * (n - j); // 上下连边
} else {
ans += verticalCut[j++] * (m - i); // 左右连边
}
}
return ans;
}
};
数据范围没有搞清楚,浪费了很多时间在上面。对于从 src1,src2 -> dest 的路径来说,必然是一个二叉路(可以在特殊情况下退化成链表)。那么整个子图的最小权重和就应该是计算从 src1 -> X, src2 -> X, X -> dst 的距离之和。这样就进行三次 dijkstra 即可。
C++
class Solution {
vector<vector<pair<int,int>>> graph;
vector<vector<pair<int,int>>> revGraph;
int n;
static constexpr long long MAXN = 0x7f7f7f7f7f7f7f7f;
public:
vector<long long> dijkstra(vector<vector<pair<int,int>>> & graph, int src) {
vector<long long> distance(n, MAXN / 3);
distance[src] = 0;
priority_queue<pair<long long,int>, vector<pair<long long,int>>, greater<>> pq;
pq.emplace(0, src);
while(!pq.empty()) {
const auto [dist, cur] = pq.top();
pq.pop();
if(dist > distance[cur]) continue;
for(const auto & [adj, w]: graph[cur]) {
if(distance[adj] > dist + w) {
distance[adj] = dist + w;
pq.emplace(distance[adj], adj);
}
}
}
return distance;
}
long long minimumWeight(int num, vector<vector<int>>& edges, int src1, int src2, int dest) {
n = num;
graph = vector<vector<pair<int,int>>>(n);
revGraph = vector<vector<pair<int,int>>>(n);
for(int i = 0; i < edges.size(); i++) {
int u = edges[i][0], v = edges[i][1], w = edges[i][2];
graph[u].emplace_back(v,w);
revGraph[v].emplace_back(u,w);
}
vector<long long> d1 = dijkstra(graph, src1);
vector<long long> d2 = dijkstra(graph, src2);
vector<long long> d3 = dijkstra(revGraph, dest);
long long ans = MAXN / 3;
for(int i = 0; i < n; i++) {
ans = min((long long)d1[i]+d2[i]+d3[i], ans);
}
return ans < MAXN / 3 ? ans : -1;
}
};
休息一下开始看项目写文档 😵💫
和朋友畅玩 minecraft,作息混乱(2 点才睡),今天要早一些睡觉。
但是 minecraft 真的好玩 ![]()
アイラちゃん 同意 ![]()
学习了一下 GoogleTest,把自己虚拟机的工具链更新到 clang20(废了好大劲) 😵💫
但是自主学习的时光是快乐的,一边学一边摸鱼,一会儿再刷刷 B 站,堕落的人生呀 😋
Notes
GoogleTest
这里学习一下相关 googleTest 的功能。
全文来源于:GoogleTest。笔者只进行翻译和自我理解。
安装与启动
首先我们创建一个属于自己的工作区(文件夹)。创建工作区的部分可以参考 配置 ,不同系统上略有不同但 unix 类型系统都差不多。windows 是异端!。
接下来撰写我们的 cmake 文件。首先来安装相关的库。
# 最小版本要求
cmake_minimum_required(VERSION 3.22)
# 自定义 debug 模式参数。
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 -Wall")
# 设定编译器和 C++ 标准
set(CMAKE_C_COMPILER "/usr/bin/clang")
set(CMAKE_CXX_COMPILER "/usr/bin/clang++")
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD 20)
# 项目名。
project(main)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
FetchContent_MakeAvailable(googletest)
set(ENV{HTTP_PROXY} "http://localhost:1080") # 没办法,gfw
然后我们编写一个简单的测试程序
#include <gtest/gtest.h>
// Demonstrate some basic assertions.
TEST(HelloTest, BasicAssertions) {
// Expect two strings not to be equal.
EXPECT_STRNE("hello", "world");
// Expect equality.
EXPECT_EQ(7 * 6, 42);
}
最后增加以下部分开启 googletest。
enable_testing() # 开启测试
add_executable(main main.cpp) # 增加可执行文件和其源文件。
target_link_libraries(
main
GTest::gtest_main
) # 链接 gtest 动态库。
include(GoogleTest) # 包含头文件。
gtest_discover_tests(main) # 自动寻找测试项目。
从源码安装
git clone https://github.com/google/googletest.git
cd googletest/
mkdir build
cd build
cmake ..
make
sudo make install
cmake 脚本变成:
cmake_minimum_required(VERSION 3.22)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 -Wall")
set(CMAKE_C_COMPILER "/usr/bin/clang")
set(CMAKE_CXX_COMPILER "/usr/bin/clang++")
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD 20)
project(main)
enable_testing()
find_package(GTest REQUIRED)
add_executable(main main.cpp)
target_link_libraries(
main
GTest::gtest_main
)
include(GoogleTest)
gtest_discover_tests(main)
测试的一些基本原则
- 独立/可重复。
- 具有良好的结构,可以反应测试代码的结构。
- 可重用且可移植。
- 提示足够多的相关问题的信息,当测试失败的时候,可以提示该测试失败的信息,并且继续执行其他部分。
- 专注于测试的内容而非其他的一些杂项。
- 足够快。
基本概念
一般而言,googleTest 将从断言开始书写。断言将会判断你的一个语句是否是真。
- 断言的结果一般有三种:
- (非致命性)失败,(致命性)失败与成功。
- 当一个致命性失败出现时,当前函数将会中断。否则程序将会继续正常运行。
测试通过断言来验证代码的行为。当测试故障或出现一个(致命性)错误断言时,测试程序中止,否则它将会成功。
一个测试组 (test suites) 将会包括一个或者多个测试,你可以将测试集合成多个测试组来反应测试代码的结构。多个测试在同一组中采用共享对象和子进程时,可以放入一个 test fixture 类中。
一个测试程序可以有多个测试组 (test suites)。
断言
主要分为两种:ASSERT_*(抛出致命性错误),EXPECT_*(抛出非致命性错误)。前者需要注意可能的内存泄漏问题,因为函数将会提前终止。
我们可以通过 << 来输出错误时我们的报错信息。例如:
EXPECT_EQ(7 * 5, 42) << "The calculation result should be 42!\n";是非致命性报错,因此会继续往下执行到达程序结束。

任何的可以通过 ostream 输出的方式都可以通过断言输出。
简单测试
创建一个测试:
- 使用
TEST()宏来定义和命名一个测试函数。这是常用的 C++ 的函数,并且没有返回值。 - 在这个函数中,任何合法的 C++ 语句都可以被 include,并且采用断言检查值。
- 测试结果通过断言进行决定。如果任何断言致命性失败,或者测试程序崩溃,整个测试就将会失败。否则,它将会执行结束。
TEST(TestSuiteName, TestName) {
// Test Body
}
我们可以写出一个简单的阶乘测试
#include <gtest/gtest.h>
template<class T>
T factorial(T n) {
if(n == 1 || n == 0) return 1;
else return factorial(n-1) * n;
}
TEST(MyTest, TestFactorialCase) {
// Case statement.
EXPECT_EQ(factorial(0), 1);
EXPECT_EQ(factorial(1), 1);
}
TEST(MyTest, TestFactorialNormal) {
// Some cases.
EXPECT_EQ(factorial(1), 1);
EXPECT_EQ(factorial(2), 2);
EXPECT_EQ(factorial(3), 6);
EXPECT_EQ(factorial(8), 40320);
}

Test Fixtures:多测试中采用相同数据配置
当你发现你在相同数据上写两个/多个测试,并且需要用到相同的数据配置时,可以使用 test fixture,这样在不同测试中可以使用相同的对象配置。
创建一个 Fixture:
- 从
testing::Test中继承一个新的类。从 protected 部分开始编写。 - 在类中,可以声明任意的对象。
- 如果必要,书写一个默认的构造函数
SetUp()来对每一个测试进行准备。注意拼写! - 如果必要,书写一个析构函数来销毁所有的在构造函数中分配的资源。
- 如有必要,构建子程序来告诉你的测试进行共享。
TestSuite 采用如下的格式
TEST_F(TestFixtureClassName, TestName) {
// 测试函数体
}
这样我们可以编写示例函数:
#include <gtest/gtest.h>
#include <queue>
class QueueTest: public testing::Test {
protected:
std::queue<int> q0, q1, q2;
QueueTest() {
q1.push(1);
q2.push(2);
q2.push(3);
}
};
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(q0.size(), 0);
}
TEST_F(QueueTest, DequeueWorks) {
EXPECT_EQ(q0.empty(), true);
int n = q1.front();
EXPECT_EQ(n, 1);
q1.pop();
EXPECT_EQ(q1.size(), 0);
n = q2.front();
EXPECT_EQ(n, 2);
q2.pop();
EXPECT_EQ(q2.size(), 1);
}
添加 main 函数
我们可以将 google test 集成到 main 函数中。这样我们可以实现执行其他部分的功能。main 函数的一个实例如下:
int main() {
testing::InitGoogleTest();
int status = RUN_ALL_TESTS();
return status;
}
status = 0 代表执行成功,否则其他情况将返回 1.
更进一步
接下来将进入更多的技巧,例如复杂的失败信息,传递致命性失败信息,重用和提高测试集合速度,以及采用不同的 flags。
更多的断言
- 显式的成功和失败。
SUCCEED(),FAIL()(致命性),ADD_FAILURE(file, line)(非致命性)。ADD_FAILURE_AT()在指定文件和行后增加非致命性信息。
- 异常断言
EXPECT/ASSERT_THROW(statement, exception_type)验证抛出特定类型的错误。
复杂分枝
采用一以下函数来提供比 EXPECT_TRUE(statement) 更好的消息。
testing::AssertionResult IsEven(int n) {
if ((n % 2) == 0)
return testing::AssertionSuccess();
else
return testing::AssertionFailure() << n << " is odd";
}
// bool IsEven(int n) {
// return (n % 2) == 0;
// }
浮点数比较
采用以下方式来进行简单但是比较全面的比较。
using ::testing::FloatLE;
using ::testing::DoubleLE;
// ...
EXPECT_PRED_FORMAT2(FloatLE, val1, val2);
EXPECT_PRED_FORMAT2(DoubleLE, val1, val2);
アイラちゃん 是 teleai 吗?我下周也有中国电信的 ai infra 面试,来偷吃点面筋 😋
主治医师李大华 不难的,放心~只要有的聊就好 😋
アイラちゃん 下周二一面,二面也没有算法吗,对面告诉我好像是写 rust + ai infra
今天晚上遇到了很恶心的 leetcode 题,先回宿舍再写 😵💫
欧拉路径: 通过图中所有边恰好一次且行遍所有顶点 (允许多次经过同一个点)的通路 称为欧拉路径。即一笔画。
欧拉回路:起点和终点相同的欧拉路径。
- 无向图:因为欧拉路径中,除了起点与终点以外,任意点的“进”“出”次数相等,所以除了两个点为奇点(度数为奇数的点)(终点和起点)以外,其它点的度数均为偶数。如果是欧拉回路,奇点的个数应该为 0。
- 有向图:欧拉路径中,最多只有两个点的入度不等于出度。起点出度比入度大 1,终点入度比出度大 1。如果是欧拉回路,所有点的 入度 = 出度。
这样我们使用 HierHolzer 算法。
- 判断奇点数。奇点数若为 0 则任意指定起点,奇点数若为 2 则指定起点为奇点。
- 开始递归函数 Hierholzer(x):
2.1 循环寻找与 x 相连的边 x→u:
2.2 删除 x→u,如果是无向图则还要删除 u→x。
2.3 递归进入 Hierholzer(u);
2.4 回溯时将 x 插入答案中 - 倒序输出答案队列
经典的 HierHolzer 算法。这里我们先用比较耗时的标记法实现最朴素的算法。其实这是不正确的,因为仍然会导致遍历时间复杂度渐进到 。必须进行删除操作。
C++ 错误
class Solution {
unordered_map<string, vector<pair<string, bool>>> graph;
vector<string> ans;
public:
void Hierholzer(string & v) {
if(graph.find(v) != graph.end()) {
for(auto & [adj, isValid]: graph[v]) {
if(isValid) {
isValid = false;
Hierholzer(adj);
}
}
}
ans.push_back(v);
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for(const auto & edge: tickets) {
string u = edge[0], v = edge[1];
graph[u].emplace_back(v, true);
}
for(auto & vec: graph) {
ranges::sort(vec.second, [](const pair<string, bool> & p1, const pair<string, bool> & p2) -> const bool {
return p1.first < p2.first;
});
}
string start = "JFK";
Hierholzer(start);
reverse(ans.begin(), ans.end());
return ans;
}
};
C++ 正确
class Solution {
unordered_map<string, vector<string>> graph;
vector<string> ans;
public:
void Hierholzer(string & v) {
if(graph.find(v) != graph.end()) {
while(!graph[v].empty()) {
string adj = graph[v].back();
graph[v].pop_back();
Hierholzer(adj);
}
}
ans.push_back(v);
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for(const auto & edge: tickets) {
string u = edge[0], v = edge[1];
graph[u].emplace_back(v);
}
for(auto & vec: graph) {
ranges::sort(vec.second, [](const string & s1, const string & s2) -> const bool {
return s1 > s2;
});
}
string start = "JFK";
Hierholzer(start);
reverse(ans.begin(), ans.end());
return ans;
}
};
超级坑,官方解法也有可能超时。手动关闭 address sanitizer. __attribute__((no_sanitize_address))
经过和 🥕 群友的讨论后发现瓶颈在动态分配上。应该从开头就进行内存分配。感谢 🥕 群友。动态分配 *2 非常耗时。
// vector<vector<int>> ans;
vector<vector<int>> ans(pairs.size(),vector<int>(2));
int i = 0;
auto Hierholzer = [&](this auto && Hierholzer, int u) -> void {
while (!graph[u].empty()) {
int v = graph[u].back();
graph[u].pop_back();
Hierholzer(v);
// ans.push_back({u, v});
ans[i][0]=u, ans[i++][1]=v;
}
};
Hierholzer(start);
C++ Recommended
class Solution {
public:
vector<vector<int>> validArrangement(vector<vector<int>>& pairs) {
unordered_map<int, vector<int>> graph;
unordered_map<int, int> indegree, outdegree;
for (const auto & p: pairs) {
int u = p[0], v = p[1];
graph[u].push_back(v);
++indegree[v];
++outdegree[u];
}
int start = pairs[0][0];
for (const auto& [cur, out_deg]: outdegree) {
if (out_deg == indegree[cur] + 1) {
start = cur;
break;
}
}
// vector<vector<int>> ans;
vector<vector<int>> ans(pairs.size(),vector<int>(2));
int i = 0;
auto Hierholzer = [&](this auto && Hierholzer, int u) -> void {
while (!graph[u].empty()) {
int v = graph[u].back();
graph[u].pop_back();
Hierholzer(v);
// ans.push_back({u, v});
ans[i][0]=u, ans[i++][1]=v;
}
};
Hierholzer(start);
reverse(ans.begin(), ans.end());
return ans;
}
};
C++ Deprecated
class Solution {
public:
vector<vector<int>> validArrangement(vector<vector<int>>& pairs) {
unordered_map<int, vector<int>> graph;
unordered_map<int, int> indegree, outdegree;
for (const auto & p: pairs) {
int u = p[0], v = p[1];
graph[u].push_back(v);
++indegree[v];
++outdegree[u];
}
int start = pairs[0][0];
for (const auto& [cur, out_deg]: outdegree) {
if (out_deg == indegree[cur] + 1) {
start = cur;
break;
}
}
vector<vector<int>> ans;
auto Hierholzer = [&](this auto && Hierholzer, int u) -> void {
while (!graph[u].empty()) {
int v = graph[u].back();
graph[u].pop_back();
Hierholzer(v);
ans.push_back({u, v});
}
};
Hierholzer(start);
reverse(ans.begin(), ans.end());
return ans;
}
};
今天的每日一题
稍微绕一点脑筋。我们需要排序,随后判断什么时候最小值和最大值的差大于限定值,随后再将分段 +1 即可。
C++
class Solution {
public:
int partitionArray(vector<int>& nums, int k) {
ranges::sort(nums);
int ans = 0;
int mini = nums[0], maxi = nums[0];
for(int i = 1; i < nums.size(); i++) {
mini = min(mini, nums[i]);
maxi = max(maxi, nums[i]);
if(maxi - mini > k) {
ans += 1;
maxi = nums[i];
mini = nums[i];
}
}
return ++ans;
}
};
祝大家晚安~
X: @ima_AI123

主治医师李大华 算法最好也准备一下,有备无患,我是两次面试都没叫手撕算法,而且已经拿到 offer 啦 ![]()
アイラちゃん 好奇你的图图是哪里获取的捏(喜欢😍
😀 X: @ima_AI123
开大组会,开宣讲会,开小组会,开实习会,到现在才把材料和会给搞完,好累啊
随机做几道题就结束吧,想去打 Minecraft 了 😋
🫳 🐟️
写面条代码出来了。
C++
class Solution {
public:
int maxDistance(string s, int k) {
int sx=0,sy=0;
vector<int> t(4);
int ans=0;
for(auto it:s){
if(it=='N'){
++sx;
t[0]++;
}
else if(it=='S'){
--sx;
t[1]++;
}
else if(it=='E'){
++sy;
t[2]++;
}
else{
--sy;
t[3]++;
}
if(sx>=0){
if(sy>=0){
ans=max(ans,abs(sx)+abs(sy)+min(k,t[1]+t[3])*2);
}
else{
ans=max(ans,abs(sx)+abs(sy)+min(k,t[1]+t[2])*2);
}
}
else{
if(sy>=0){
ans=max(ans,abs(sx)+abs(sy)+min(k,t[0]+t[3])*2);
}
else{
ans=max(ans,abs(sx)+abs(sy)+min(k,t[0]+t[2])*2);
}
}
}
return ans;
}
};
很经典的单调栈求取最大矩形面积。类似的很喜欢面试时恶心人的还有接雨水。
C++
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
vector<int> left(n),right(n);
stack<int> firstIndex;
for(int i = 0; i < n; i++)
{
while(!firstIndex.empty() && heights[firstIndex.top()] >= heights[i])
firstIndex.pop();
left[i] = firstIndex.empty() ? -1 : firstIndex.top();
firstIndex.push(i);
}
firstIndex = stack<int>();
for(int i = n-1; i >= 0; i--)
{
while(!firstIndex.empty() && heights[firstIndex.top()] >= heights[i])
firstIndex.pop();
right[i] = firstIndex.empty() ? n : firstIndex.top();
firstIndex.push(i);
}
int area = 0;
for(int i = 0; i < n; i++)
{
area = max(area, heights[i] * (right[i] - left[i] - 1));
}
return area;
}
};
观察:1 2 3 4 5 6 7 -> 1 7 2 6 3 5 4
- 1 2 3 4 5 6 7 -> 1 2 3 4 // 5 6 7
- 1 2 3 4 // 5 6 7 -> 1 2 3 4 // 7 6 5
- 1 7 2 6 3 5 4
记住三步走:
- 寻找中点(快慢指针)
- 反转后半链表。
- 链接两个链表。
水平太差经常内存越界访问 😵💫
C++
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode * findMid(ListNode * head) {
ListNode * slow = head, * fast = head;
while(fast && fast -> next) {
slow = slow -> next;
fast = fast -> next -> next;
}
ListNode * ans = slow -> next;
slow -> next = nullptr;
return ans;
}
ListNode * reverseList(ListNode * head) {
if(head == nullptr) return nullptr;
ListNode * pre = nullptr, * cur = head, * next = cur -> next;
while(next != nullptr) {
cur -> next = pre;
pre = cur;
cur = next;
next = next -> next;
}
cur -> next = pre;
return cur;
}
void mergeLists(ListNode * l1, ListNode * l2) {
if(l2 == nullptr) return;
ListNode * next = l1 -> next, * temp;
while(next) {
l1 -> next = l2;
// swap;
l1 = l2;
l2 = next;
next = l1 -> next;
}
l1 -> next = l2;
}
void reorderList(ListNode* head) {
ListNode * l1 = head, * l2;
l2 = findMid(head);
/********************** DEBUG ************************/
// ListNode * print = head;
// cout << "l1" << endl;
// while(print) {
// cout << print -> val << ' ';
// print = print -> next;
// }
/********************** END OF DEBUG ************************/
l2 = reverseList(l2);
/********************** DEBUG ************************/
// cout << endl;
// cout << "l2" << endl;
// ListNode * print = l2;
// while(print) {
// cout << print -> val << ' ';
// print = print -> next;
// }
/********************** END OF DEBUG ************************/
mergeLists(l1, l2);
/********************** DEBUG ************************/
// ListNode * print = head;
// cout << "l1" << endl;
// while(print) {
// cout << print -> val << ' ';
// print = print -> next;
// }
/********************** END OF DEBUG ************************/
}
};
好的,谢谢你!
アイラちゃん 我也想 MC 了,可是还有考试 💫
アイラちゃん 内存越界访问
看出来确实很想 mc 了
复习 tarjan 感觉跟没学过一样,再加上 OIer 太聪明,代码风格非常标新立异,给我写麻了,原谅我这个 🅱️ 🥚 吧
アイラちゃん 我的手工艺品(minecraft)
Hard time comes again and never leave.
今天学 tarjan 算法。
与 kosaraju 算法类似,tarjan 算法可以用来寻找图中的强连通分量。kosaraju 算法通过对图进行一次 DFS,对反图再进行一次 DFS 来判断图中的强连通分量,而 tarjan 算法则只需要一次 DFS 遍历即可找到强连通分量。
回顾相关的 DFS 树知识

Tarjan 算法基于以下的既定事实:
- DFS 算法将会生成一个 DFS 树。
- 强连通分量将会构成 DFS 树的一个子树。
- 如果我们能在这样一个子树中找到子树的根,我们就能够获得整个强连通分量。
- 没有从一个强连通分量指向另一个强连通分量的回边。
- 两个强连通分量之间可能有横边但是横边不会再 DFS 过程中使用到。
在 Tarjan 算法中,我们主要需要维护两个数组 和 .
- 代表了在 DFS 访问过程中该节点被访问到的时间 Ts。
- 包含 的 SCC 中第一个被搜索到的节点的 值。
这样在 Tarjan 算法中我们有如下的维护方式:
Tarjan
class Tarjan {
vector<int> low, dfn, instack, color;
int dfnCnt, scc;
vector<vector<int>> graph;
stack<int> st;
public:
void tarjan(int cur) {
low[cur] = dfnCnt + 1;
dfn[cur] = ++dfnCnt;
instack[cur] = true;
st.push(cur);
for(const auto & adj: graph[cur]) {
if(dfn[adj] == 0) {
tarjan(adj);
low[cur] = min(low[cur], low[adj]); // 没有访问过,于是寻找邻边和自己所在子树中的最早访问的点。
} else if(instack[adj]) {
low[cur] = min(low[cur], dfn[adj]); // 访问过,则判断最先访问的是什么。(判断回边)。
}
}
if(low[cur] == dfn[cur]) // 是 SCC 中的第一个被访问的节点
{
color[cur] = ++scc;
while(st.top() != cur) {
// 染色,弹栈
color[st.top()] = scc;
instack[st.top()] = false;
st.pop();
}
instack[cur] = false;
st.pop(); // 最后把 u 弹出去
}
}
};
求取割点(删除后增加了强连通分量)。
Tarjan CutPoint
class TarjanCutVertex {
vector<vector<int>> graph;
vector<int> dfn, low;
vector<int> ans;
int dfnCnt;
public:
void tarjan(int cur, int pa) {
low[cur] = dfnCnt + 1, dfn[cur] = ++dfnCnt;
int child = 0;
bool flag = false;
for(const auto & adj: graph[cur]) {
if(!dfn[cur]) {
child++;
tarjan(adj, cur);
low[cur] = min(low[cur], low[adj]);
// 如果不是自己,且不通过父亲返回的最小点符合割点的要求,并且没有被标记过
// 要求即为:删了父亲连不上去了,即为最多连到父亲
if(pa != -1 && low[adj] >= dfn[cur] && !flag) {
ans.push_back(cur);
flag = true;
}
} else if(adj != pa) {
low[cur] = min(low[cur], dfn[adj]);
}
}
// 主要代码,自己的话需要 2 个儿子才可以
if(!flag && pa == -1 && child >= 2) {
ans.push_back(cur);
flag = true;
}
}
};
求取割边。
Tarjan cut edge
class Solution {
vector<int> dfn, low;
vector<vector<int>> graph;
vector<vector<int>> ans;
int n;
int dfnCnt; // The dfs num of the current vertex.
public:
void tarjan(int cur, int pa) {
low[cur] = dfnCnt + 1;
dfn[cur] = ++dfnCnt;
for(const auto & adj: graph[cur]) {
if(dfn[adj] == 0) {
tarjan(adj, cur);
low[cur] = min(low[cur], low[adj]);
if(low[adj] > dfn[cur]) {
ans.push_back({adj, cur});
}
} else if(adj != pa) {
low[cur] = min(low[cur], dfn[adj]);
}
}
}
};
明天再看双连通分量吧,好难啊
点双连通:在图中任意删去一个点都不会破坏这个点对的连通性。成为点双连通分量。不具有传递性。
边双连通:在图中任意删去一条边都不会破坏这个点对的连通性。成为边双连通分量。具有传递性。
无向图中他们的极大子图则为互相的双连通分量。
点双连通
class TarjanVertexBCC {
vector<int> low, dfn;
vector<bool> isCut;
int dfnCnt, dcc;
vector<vector<int>> graph;
stack<int> st;
vector<vector<int>> dccVecs; // The stored dccs.
public:
void tarjan(int cur) {
low[cur] = dfnCnt + 1;
dfn[cur] = ++dfnCnt;
int child = 0;
st.push(cur);
for(auto & adj: graph[cur]) {
if(dfn[adj] == 0) {
tarjan(adj);
low[cur] = min(low[cur], low[adj]);
if(low[adj] >= dfn[cur]) {
if(++child > 1 || cur != 0) isCut[cur] = true;
dccVecs.push_back({});
int t = -1;
do {
t = st.top();
st.pop();
dccVecs[dcc].push_back(t);
} while(t != adj);
dccVecs[dcc].push_back(cur);
dcc++;
}
} else {
low[cur] = min(low[cur], dfn[adj]);
}
}
}
};
``
</details>
<details><summary>边双连通</summary>
```c++
class TarjanEdgeBCC {
vector<int> dfn,low;
vector<bool> bridge, visited;
vector<vector<int>> dcc;
vector<vector<pair<int,int>>> graph; /// cur -> (adj, idx);
int dfnCnt;
void dfs(int cur) {
visited[cur] = true;
dcc.back().push_back(cur);
for(const auto & [adj, idx]: graph[cur]) {
if(!bridge[idx] && !visited[adj]) {
dfs(adj);
}
}
}
void tarjan(int cur, int id) {
low[cur] = dfnCnt + 1;
dfn[cur] = ++dfnCnt;
for(const auto & [adj, idx]: graph[cur]) {
if(!dfn[adj]) {
tarjan(adj, idx);
low[cur] = min(low[cur], low[adj]);
} else if(id != idx) {
low[cur] = min(low[cur], dfn[adj]);
}
}
if(dfn[cur] == low[cur] && id != -1) {
bridge[id] = true;
}
}
};
寻找割边。
C++
class Solution {
/**
* Using tarjan algorithm to find the cut edge.
*
* dfn: Record the sequence of the particular vertex.
* low: The smallest number of dfn in the SUBTREE of u.
*/
vector<int> dfn, low;
vector<vector<int>> graph;
vector<vector<int>> ans;
int n;
int dfnCnt; // The dfs num of the current vertex.
public:
void tarjan(int cur, int pa) {
low[cur] = dfnCnt + 1;
dfn[cur] = ++dfnCnt;
for(const auto & adj: graph[cur]) {
if(dfn[adj] == 0) {
tarjan(adj, cur);
low[cur] = min(low[cur], low[adj]);
if(low[adj] > dfn[cur]) {
ans.push_back({adj, cur});
}
} else if(adj != pa) {
low[cur] = min(low[cur], dfn[adj]);
}
}
}
vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
this -> n = n;
graph = vector<vector<int>>(n);
dfn = vector<int>(n, 0);
low = vector<int>(n, 0);
for(const auto & edge: connections) {
int u = edge[0], v = edge[1];
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
tarjan(0, -1);
return ans;
}
};
判断割点。存在割点就代表可以仅通过一次操作将图变成多个强连通分量。
C++
constexpr int directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
class Solution {
unordered_map<int, vector<int>> graph;
unordered_map<int, int> dfn, low;
vector<int> ans;
int dfnCnt;
public:
void tarjan(int cur, int pa) {
low[cur] = dfnCnt + 1, dfn[cur] = ++dfnCnt;
int child = 0;
bool flag = false;
if(graph.find(cur) != graph.end()) {
for(const auto & adj: graph[cur]) {
if(dfn.find(adj) == dfn.end()) {
child++;
tarjan(adj, cur);
low[cur] = min(low[cur], low[adj]);
// Key part 1.
if(pa != -1 && low[adj] >= dfn[cur] && !flag) {
ans.push_back(cur);
flag = true;
}
} else if(adj != pa) {
low[cur] = min(low[cur], dfn[adj]);
}
}
// Key part 2.
if(!flag && pa == -1 && child >= 2) {
ans.push_back(cur);
flag = true;
}
}
}
int minDays(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
dfnCnt = 0;
// Build graph.
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 1) {
// 4 directions.
graph[i * n + j] = vector<int>();
for(const auto & dir: directions) {
int nx = i + dir[0], ny = j + dir[1];
if(nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
graph[i * n + j].push_back(nx * n + ny);
}
}
}
}
}
if(graph.size() == 0) return 0;
else if(graph.size() == 1) return 1;
// do tarjan with each vertex.
int sccCnt = 0;
for(const auto & [cur, adjs]: graph) {
if(dfn.find(cur) == dfn.end()) {
sccCnt++; // New strong connected components.
tarjan(cur, -1);
}
}
if(sccCnt > 1) return 0;
else if(ans.size() > 0) return 1;
return 2;
}
};
为什么 1192 和 1568 对于 Tarjan 的使用不同呢?因为 tarjan 图并不一定连通,因此需要对每个点进行 dfs,跳过已经访问过的节点。而 1192 是保证连通的。
太难了还没学会双连通分量。(有点双连通,边双连通)
先把问题简化成树。在树上的节点删除时还需要保持连通性,则需要考虑点双连通情况。基于以下几个事实:
- 将所有单独的点双连通分量缩点后将形成一颗树。
- 点双连通分量上的割点绝对不是缩点后形成的叶子节点。(如果他是叶子节点,则说明删除了他后将不会使得连通分量增加,这就与割点的定义相互矛盾。割点删除后连通分量将会增加。
这样先进行点双连通分量 + 割点的 tarjan 缩图,随后选择每个叶子节点(非割点的点双连通分量中节点的最小值),将他们累加即可。
C++
class Solution {
vector<int> low, dfn;
vector<bool> isCut;
int dfnCnt, dcc;
vector<vector<int>> graph;
stack<int> st;
vector<vector<int>> dccVecs; // The stored sccs.
public:
void tarjan(int cur) {
low[cur] = dfnCnt + 1;
dfn[cur] = ++dfnCnt;
int child = 0;
st.push(cur);
for(auto & adj: graph[cur]) {
if(dfn[adj] == 0) {
tarjan(adj);
low[cur] = min(low[cur], low[adj]);
if(low[adj] >= dfn[cur]) {
if(++child > 1 || cur != 0) isCut[cur] = true;
dccVecs.push_back({});
int t = -1;
do {
t = st.top();
st.pop();
dccVecs[dcc].push_back(t);
} while(t != adj);
dccVecs[dcc].push_back(cur);
dcc++;
}
} else {
low[cur] = min(low[cur], dfn[adj]);
}
}
}
long long minimumCost(vector<int>& cost, vector<vector<int>>& roads) {
int n = cost.size();
// Initialize common data structure.
dfn = vector<int>(n,0), low = vector<int>(n,0);
isCut = vector<bool>(n,false);
graph = vector<vector<int>>(n);
dfnCnt = 0, dcc = 0;
// Build graph.
for(const auto & edge: roads) {
int u = edge[0], v = edge[1];
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
// tarjan.
tarjan(0);
// SCC only one. choose minimum.
if(dcc == 1) {
long long ans = LLONG_MAX / 2;
for(const auto & i: cost) {
ans = min(ans, (long long)i);
}
return ans;
}
// Tarjan squeeze SCC into 1.
vector<int> newVertex;
for(const auto & vec: dccVecs) {
long long count = 0, minCost = LLONG_MAX;
for(const auto & adj: vec) {
if(isCut[adj]) count++;
else minCost = min(minCost, (long long)cost[adj]);
}
if(count == 1) {
newVertex.push_back(minCost);
}
}
ranges::sort(newVertex);
long long ans = 0;
for(int i = 0; i < newVertex.size() - 1; i++) {
ans += newVertex[i];
}
return ans;
}
};
比较容易看出,首先统计字频,然后选择一个字符作为删除完成后的最少字符对其进行贪心即可。对于选定的 freq,如果其他字符的频率:
y < freq: 说明要全部删除。
y > freq + k: 删除掉超出范围的字符。
C++
class Solution {
public:
int minimumDeletions(string word, int k) {
// Count freq.
unordered_map<char, int> charToFreq;
for(const char ch: word) {
charToFreq[ch]++;
}
vector<int> freqs(charToFreq.size());
int i = 0;
for(const auto & [_, vals]: charToFreq) {
freqs[i++] = vals;
}
int ans = INT_MAX;
for(int i = 0; i < freqs.size(); i++) {
int tmp = 0;
for(int j = 0; j < freqs.size(); j++) {
if(freqs[j] < freqs[i]) {
tmp += freqs[j];
} else if(freqs[j] > freqs[i] + k) {
tmp += freqs[j] - freqs[i] - k;
}
}
ans = min(tmp, ans);
}
return ans;
}
};
Tarjan 学了好久,赶紧去补数学了 😭,都是 🫳 🐟️的错❌
今天学了一下无锁队列(单生产者单消费者),也算做边摸鱼边学了。
Notes
Single Producer single consumer Lock-Free FIFO(Cppcon-23)
一些经典的 SPSC 无锁队列
- boost: spsc_queue.hpp
- DPDK
- rigtorp 的实现
- 1024cores
单生产者单消费者
- 一个单独的写进程(生产者)
- 一个单独的读进程(读者)
- 无锁:不使用互斥锁。在任意时间,相同的线程将运行。
- 无等待:每个进程不会因为其他线程而阻塞。
- 循环 buffer:一个单独的,固定大小的 buffer。
SPSC 使用
- 从网络中读取到了信息,进入 SPSC 队列。
- 从 SPSC 中获取消息到处理信息程序中。
- 处理完成后再进入 SPSC 队列,并从 SPSC 中提取信息写入网络。
基础的队列(这里是我自己的实现)
我们首先先完成一个没有进行任何数据保护的队列。首先我们定义一个简单的队列(没有采用模板)
#ifndef QUEUE_HPP_
#define QUEUE_HPP_
#include <cstdlib>
template<class T>
class Queue {
private:
int capacity;
int pushPtr;
int popPtr;
T * data;
public:
Queue(const Queue && q) = delete;
Queue && operator=(const Queue && q) = delete;
Queue(): capacity(0), pushPtr(0), popPtr(0), data(nullptr) {}
Queue(int cap): capacity(cap + 1), pushPtr(0), popPtr(0) {
data = new int [capacity];
}
~Queue() {
capacity = 0;
pushPtr = 0;
popPtr = 0;
if(data != nullptr)
delete [] data;
}
void push(T elem) {
if(pushPtr - popPtr == capacity) {
throw "FIFO has full.\n";
}
data[pushPtr % capacity] = elem;
pushPtr++;
}
void pop() {
if(pushPtr == popPtr) {
throw "Empty FIFO.\n";
}
popPtr++;
}
int front() {
if(pushPtr == popPtr) {
throw "Empty FIFO.\n";
}
return data[popPtr % capacity];
}
bool empty() {
return pushPtr == popPtr;
}
int size() {
return pushPtr - popPtr;
}
};
#endif
随后我们编写一些简单的测试程序。我们采用了三个 test 程序分别如下:
#include <gtest/gtest.h>
#include <random>
#include <thread>
#include <vector>
#include <numeric>
#include "Queue.hpp"
using QueueInt = Queue<int>;
TEST(QueueTest, PushTest) {
QueueInt q(10);
for(int i = 0; i < 10; i++) {
q.push(i);
}
EXPECT_EQ(q.size(), 10);
}
TEST(QueueTest, PopTest) {
QueueInt q(10);
for(int i = 0; i < 10; i++) {
q.push(i);
}
int i = 0;
while(!q.empty()) {
EXPECT_EQ(q.front(), i);
EXPECT_EQ(q.size(), 10 - i);
q.pop();
i++;
}
}
TEST(QueueTest, PushAndPopRandomTest) {
std::vector<int> vec(10);
iota(vec.begin(), vec.end(), 0);
std::random_device rd;
std::mt19937 gen(rd());
std::shuffle(vec.begin(), vec.end(), gen);
QueueInt q(10);
for(const auto i: vec) {
q.push(i);
}
EXPECT_EQ(q.size(), 10);
int i = 0;
while(!q.empty()) {
EXPECT_EQ(q.front(), vec[i]);
EXPECT_EQ(q.size(), 10 - i);
q.pop();
i++;
}
EXPECT_TRUE(q.empty());
EXPECT_EQ(q.size(), 0);
}

Lock 加锁
以上的程序在多线程情况上会出现错误。例如我们增加一个数据结构和测试函数用于我们的单生产者单消费者测试。我们首先仅进行插入/弹出操作并检查栈的最终大小。
class ConcurrencyTest {
std::random_device rd;
std::mt19937 gen;
public:
std::vector<int> data;
QueueInt q;
ConcurrencyTest(int n): gen(rd()), data(std::vector<int>(n)), q(n) {
iota(data.begin(), data.end(), 0);
std::shuffle(data.begin(), data.end(), gen);
}
};
void push(std::vector<int> & data, QueueInt & q) {
for(const auto & i: data) {
q.push(i);
}
}
void pop(std::vector<int> & ans, QueueInt & q, int n) {
for(int i = 0; i < n; i++) {
if(!q.empty()) {
ans.push_back(q.front());
q.pop();
}
}
}
TEST(QueueTest, ENABLED_SPSC_twoThreads) {
int n = 1e6;
ConcurrencyTest test(n);
std::vector<int> ans;
std::thread t1(push, std::ref(test.data), std::ref(test.q));
std::thread t2(pop, std::ref(ans), std::ref(test.q), n);
t1.join();
t2.join();
ASSERT_EQ(ans.size(), test.data.size());
for(int i = 0; i < test.data.size(); i++) {
EXPECT_EQ(ans[i], test.data[i]);
}
}
有可能会出现以下情况:

我们使用 ThreadSanitizer 来进行检查。

这样很明显出现了数据竞争。
当一个表达式的值写入了内存,同时另一个表达式读取或修改相同内存下的值的时候,表达式将被称为冲突。程序中两个冲突的表达式除了以下情况外将出现数据竞争。
- 两个表达式在同一进程/同一信号上执行。或者
- 两个表达式是原子表达式 (
std::atomic) 或者- 冲突表达式具有固定的执行顺序。(
std::memory_order)。
数据竞争是 UB 行为。
于是我们需要对程序进行处理。我们首先需要分析发生数据竞争的区域,也就是互斥区。我们需要关注的就是写操作和读操作出现的区域:
push部分出现了数据竞争。因为对 pushPtr 和 data 的写操作将会影响到其他线程。pop部分。对 popPtr 和 data 的写操作将影响到其他线程。size,empty,front部分。读操作可能会因为不同的执行顺序发生冲突。
最简单的方法就是增加读写锁。我们于是有:
- 成员增加一个读写锁
std::shared_mutex。 - 对于读操作,我们采用共享锁
std::shared_lock。 - 对于写操作,我们采用排他锁
std::unique_lock。 - 注意避免死锁。
这样我们有:
template<class T> class Queue {
private:
// ...
std::shared_mutex rwlock_{};
public:
// ...
void push(T elem) {
std::unique_lock lock(rwlock_);
// ...
}
void pop() {
std::unique_lock lock(rwlock_);
// ...
}
int front() {
std::shared_lock lock(rwlock_);
// ...
}
bool empty() {
std::shared_lock lock(rwlock_);
// ...
}
int size() {
std::shared_lock lock(rwlock_);
// ...
}
}
这样 threadSanitizer 并没有报告数据竞争问题。
原子化操作(存在问题)
为了每次都能通过,我们首先对 pushPtr 和 PopPtr 相关的操作进行原子化。我们首先观察我们 push, pop 和 front 的时间顺序执行情况。为了方便我们的思考,我们将 front 和 pop 操作画在了一起。

我们的数据竞争就可以很明显地看出来:load 和 store 在两个线程中的顺序并不相同。我们需要保证,store 必须发生在 load 之前。我们发现我们的 pushPtr/PopPtr 需要首先执行 load(自增)操作后才能再继续执行 pop(自增)操作。因此我们需要在最初的无锁版本上进行如下修改:
#include<atomic>
class Queue {
private:
/// ...
std::atomic<int> pushPtr;
std::atomic<int> popPtr;
static_assert(std::atomic<int>::is_always_lock_free);
// ...
};
这样我们的操作将会变成:

我们重新写了测试程序和 Queue,通过继承的方式来重构了一下,我们可以对比原子操作和带锁操作的差距。

同时还稍微看了看 value_category。正常来说所有的表达式(包括操作符与被操作数,变量,字面值)都分成三类:纯右值 (prvalue), 将亡值 (xvalue),以及左值 (lvalue)。
- 广义左值:执行后能够得到具名对象/函数的就是广义左值。
- 左值:去除掉将亡值的广义左值就是左值。
- 纯右值:1. 计算出操作数与其内建操作符的值表达式 2. 初始化一个对象。
- 将亡值:一个广义左值,并表示对象内部的资源可以被复用。(std::move(obj) 就是将亡值)。
右值包括纯右值 + 将亡值。左值包括了去除将亡值的广义左值。
在使用 thread 的过程中,还踩了很多的坑。因为 thread 的构造函数:

要求函数必须是 callable 的,如果是类内的函数就必须要 static。同样值传递也是右值传递,因此所有的引用传值都需要靠 std::ref() 来提供一个 std::refrence_wrapper<T> 引用包裹器进行传递。
今天 leetcode 很水,就把 tarjan 双连通部分弄清楚了而已 🫳 🐟️
C++
class Solution {
public:
vector<string> divideString(string s, int k, char fill) {
vector<string> res;
int n = s.size();
int cur = 0;
while (cur < n) {
res.push_back(s.substr(curr k));
cur += k;
}
res.back() += string(k - res.back().length(), fill);
return res;
}
};
明天要开始实习了,超长时间通勤(1h15min)左右,只能在地铁上碎片化学习了。还是要努力,感觉 C++ 的很多东西都没有搞得很清楚,想要学会的 infra+ 分布式目前也举步维艰,先从刷 6.5840 的 ppt 开始,继续加油吧~
看到团子了,每次都会心疼团子 😭
X: @jyabochii

感谢日记楼让我养成了一天不更就感觉《愧对》塬友于是会学到很多不一定有用但是很有趣的知识,还从塬友身上学到了自律 😋
🥰

アイラちゃん 不客气喵 ![]()
自觉接受塬友监督是アイラちゃん永葆生机活力的必由之路(刚刚在背的习概
)
舍友突发头疼,看来今晚睡不了啦
明天还要赶去实习,这下是一根筋变两头堵了

アイラちゃん 怎么图片还在上传?![]()
PipaQinse233 传太急了
幸好没什么大问题,打道回府
实习用的有 rust,看来得高速上手 rust 了 😵💫
更新:实习环境
实习环境


继续灵神的题单。今天是简单的二分图染色。我们都采用 dfs 与 bfs 方法完成。
DFS + BFS
DFS
class Solution {
public:
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
vector<int> color(n, 0);
bool flag = true;
auto dfs = [&](this auto && dfs, int from) -> void {
if(!flag) return;
int curColor = color[from];
// cout << from << " " << curColor << endl;
for(const auto & adj: graph[from]) {
if(curColor == color[adj]) {
flag = false;
return;
} else if(color[adj] == 0) {
color[adj] = (curColor == 1) ? 2 : 1;
dfs(adj);
}
}
};
for(int i = 0; i < n; i++) {
if(color[i] == 0) {
color[i] = 1;
dfs(i);
}
}
return flag;
}
};
BFS
class Solution {
public:
bool isBipartite(vector<vector<int>>& graph) {
queue<int> q;
int n = graph.size();
vector<int> color(n, 0);
for(int i = 0; i < n; i++) {
if(color[i] == 0) {
q.emplace(i);
color[i] = 1;
while(!q.empty()) {
int cur = q.front();
q.pop();
int curColor = color[cur];
for(const auto & adj: graph[cur]) {
if(color[adj] == curColor) {
return false;
} else if(color[adj] == 0) {
color[adj] = (curColor == 1) ? 2 : 1;
q.push(adj);
}
}
}
}
}
return true;
}
};
DFS+BFS
DFS
class Solution {
public:
bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
vector<vector<int>> graph(n);
for(const auto & edges: dislikes) {
int u = edges[0] - 1, v = edges[1] - 1;
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
vector<int> color(n, 0);
bool flag = true;
auto dfs = [&](this auto && dfs, int from) -> void {
if(!flag) return;
for(const auto & adj: graph[from]) {
if(color[adj] == color[from]) {
flag = false;
return;
} else if(color[adj] == 0) {
color[adj] = (color[from] == 1) ? 2 : 1;
dfs(adj);
}
}
};
for(int i = 0; i < n; i++) {
if(color[i] == 0) {
color[i] = 1;
dfs(i);
}
}
return flag;
}
};
BFS
class Solution {
public:
bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
vector<vector<int>> graph(n);
for(const auto & edges: dislikes) {
int u = edges[0] - 1, v = edges[1] - 1;
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
vector<int> color(n, 0);
queue<int> q;
for(int i = 0; i < n; i++) {
if(color[i] == 0) {
q.emplace(i);
color[i] = 1;
while(!q.empty()) {
int cur = q.front();
q.pop();
int curColor = color[cur];
for(const auto & adj: graph[cur]) {
if(color[adj] == curColor) {
return false;
} else if(color[adj] == 0) {
color[adj] = (curColor == 1) ? 2 : 1;
q.push(adj);
}
}
}
}
}
return true;
}
};
明天见~晚安
X: @ShimizuTou63646

稍微看了看 Prefill+Decode 过程。有一篇 snowflake 的论文明天看看。周末再写总结笔记。
先给项目写一个 submodules 先。
编译=摸鱼,但是运存太小了开-j8 死机了 😵💫
大战 libunwind 失败,不知道该怎么 build 了
Rust + CPP 糟糕透了,本就采用静态库的 rust 和许多需要动态库的 cpp third-parties 成功夺走了我的一天。直到下班前都没有成功 build。
很难。写了很久才写出来。主要就是分成四个区域:++/+ -/-+/- - 。找到四个分区的分界(全 0)后,分别对四个区域使用二维二分法(模板)即可。
二分本就不是我熟悉的领域,看来还要加强训练。
C++
class Solution {
public:
long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
// Binary in --, -+, +-, ++.
int n = nums1.size(), m = nums2.size();
// Matrix 0 row.
int mid_i = ranges::lower_bound(nums1, 0) - nums1.begin(), mid_j = ranges::lower_bound(nums2, 0) - nums2.begin();
using ll = long long;
vector<int> & a = nums1, & b = nums2;
auto check = [&](ll mid) -> bool {
ll cnt = 0;
if(mid < 0) {
// Right top.
int i = 0, j = mid_j;
while(i < mid_i && j < m) {
if(1ll * a[i] * b[j] > mid) {
j++;
} else {
cnt += m - j;
i++;
}
}
// left bottom.
i = mid_i; j = 0;
while(i < n && j < mid_j) {
if(1ll * a[i] * b[j] > mid) {
i++;
} else {
cnt += n - i;
j++;
}
}
} else {
cnt = 1ll * mid_i * (m - mid_j) + 1ll * (n - mid_i) * mid_j;
// left top
int i = 0, j = mid_j - 1;
while (i < mid_i && j >= 0) {
if (1LL * a[i] * b[j] > mid) {
i++;
} else {
cnt += mid_i - i;
j--;
}
}
// right bottom
i = mid_i;
j = m - 1;
while (i < n && j >= mid_j) {
if (1LL * a[i] * b[j] > mid) {
j--;
} else {
cnt += j - mid_j + 1;
i++;
}
}
}
return cnt >= k;
};
long long corners[4] = {1LL * a[0] * b[0], 1LL * a[0] * b[m - 1], 1LL * a[n - 1] * b[0], 1LL * a[n - 1] * b[m - 1]};
auto [left, right] = ranges::minmax(corners);
left--;
while (left + 1 < right) {
long long mid = left + (right - left) / 2;
(check(mid) ? right : left) = mid;
}
return right;
}
};
终于解决了 ABI 不兼容与 fPIC 问题,build 终于成功了。不枉费我一个一个拆动态库,出大问题的就三个:glog,protobuf,grpc(怎么都是你们谷歌家的产品,都是 C++ 的错 😡)
修理了一下 log 部分,让 tracing 的 log 能够重定向到终端输出。
我讨厌解决 conflicts。git merge 好难受。
明天不上班~今天修了一些单元测试,并且往单元测试中加入 tracing 日志系统来导出日志。rust 中可以通过:
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.try_init(); // 防止重复 init。
}
来实现。此外 rust 拥有自己的 atomic 类型:https://doc.rust-lang.org/std/sync/atomic/struct.AtomicUsize.html#method.load。在多线程无锁情况下,需要声明一个 atomic 类型变量,通过 fetch_and_op 操作来改变数值,通过 load/store 来读取,储存变量。并且要根据内存序。这一部分的内容我还不是很懂,要多从 C++ 入手先,此外后面还需要使用 coroutine(当然是 rust)
最后痛苦地合并 master 分支,一大堆 conflict 让人眼花缭乱,真难受。
今天的每日一题又是贪心,我们只需要不断贪'0'选'1'即可。
Rust
impl Solution {
pub fn longest_subsequence(s: String, k: i32) -> i32 {
let longest = (k as f64).log2() as usize + 1;
let mut cur = 0;
let mut count = 0;
for (i, c) in s.chars().rev().enumerate() {
if c == '1' && i < longest {
let tmp = cur + (1 << i);
if tmp > k {
continue;
} else {
cur = tmp;
count+=1;
}
} else if c == '0' {
count+=1;
}
}
count
}
}
In fact, this is sketched from C++
Rust 不语,只是指着 C++ 说:去看 C++20 吧。 😅

好忙啊,怎么什么事情都要我去做呀
首先先给项目组的多生产者多消费者队列写单元测试 (googleTest), 我将其分成了三个阶段
- 单生产者单消费者(基础功能):enqueue,dequeue,size,capacity,empty 函数。
- 基础功能压力测试:将数量提升到 i64,i128 进行测试。
- 传入指针和 unsafe 的 void *来测试是否能够抛出错误(不允许悬垂地址,空操作)
- 检测边界条件。
- 随机测试:用 mt19937_64 来生成伪随机数来判断是否正确入队出队。
- 多生产者多消费者测试。唤起 1e6 个线程,一半用于入队,一半用于出队。
- 写一个 benchmark。
然后开 pr,提交测试。下个周再学一下 CI/CD 怎么做,github actions 怎么搞。devOPS 也是完全的盲区。
然后下午又跑到大零号湾修服务器,因为所有人都无法免密登陆了。很奇怪,服务器上的.ssh 文件夹内的 authorized_keys 出现了权限问题。于是只能:
- 线下直接连接服务器。
- 一个个地输入复杂的密码登陆,打开 ssh 远程密码登陆。
- 生成跳板机公钥私钥对,生成服务器 - 跳板机公钥私钥文件。
- 写好文档告诉大家如何将私钥添加到自己的电脑上,但其实这并不安全。应该自己生成公私钥对后自己将自己独有的公钥上传服务器。但是大家都不想麻烦就算了。
- 设置好静态 IP 绑定到固定的内网,防止地址租期过期后 ip 变化。
- 调整 ssh 登陆端口,关闭密码登陆,在 FAQ 中告诉大家 ubuntu18.04 需要将 vscode 降级到 1.97 及以下,否则受到 libc++ 和 gdm 不匹配的影响无法连接。(又是 C++ 的错)。
晚上还要分析 C++ 源码,形成分析在 ARN 中如何实现动态调整链路,传递 PFC 拥塞标志,并且合并两份有冲突的拥塞控制算法的仿真代码,实在是太麻烦了。。。好忙好累啊
不管了先回去换衣服跑一会儿,今天得慢跑 4km,好久不跑步了 天塌了,下暴雨了,晚上再跑了

今晚分析了一下 ECMP 寻路算法,发现竟然是用的经典的 dijkstra 生成路由图 + 经典的哈希算法来随机选路。后面还要分析选择选择重传部分的相关代码和 API,还要分析报文是怎样修改的。
今天的 Leetcode 太难了不想做,那随机两道中等吧。
C++
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* modifiedList(vector<int>& nums, ListNode* head) {
set<int> s(nums.begin(), nums.end());
// Deal with head.
ListNode * h = head;
while(h != nullptr && s.find(h -> val) != s.end()) {
head = h -> next;
// delete h;
h = head;
}
// Deal with other node.
ListNode * iter = head, *temp;
while(iter && iter -> next) {
if(s.find(iter -> next -> val) != s.end()) {
temp = iter -> next;
iter -> next = iter -> next -> next;
delete temp;
}
else
iter = iter -> next;
}
return head;
}
};
快速幂。
C++
class Solution {
using ll = long long;
static constexpr int MOD = 1e9+7;
public:
int countGoodNumbers(long long n) {
ll ans = (n % 2 == 0) ? 1 : 5;
ll a = 20;
for(ll i = n / 2; i > 0; i >>= 1) {
if(i & 1) ans = (ans * a) % MOD;
a = a * a % MOD;
}
return ans;
}
};
感觉最近太忙太乱了,从明天开始要改变一下工作流,尽快适应高强度的开发 + 刷题也不能拉下 😵💫
アイラちゃん 怎么什么事情都要你去做啊(
看着跟初创公司一样
Zerick 感觉确实是初创
不过不奔着转正去就可以大胆尝试,感觉也好(我不想本科工作)
其实我在两线工作,一周对半开,一半在企业一半在课题组开发 🥺 想去企业主要是想积累开发经验

太晚了没有办法了 🥺
今天不想学习,就看代码写文档摆烂一天吧 😋
C++
using namespace std;
class Solution {
public:
vector<int> maxSubsequence(vector<int>& nums, int k) {
vector<pair<int,int>> temp;
for(int i = 0; i < nums.size(); i++) {
temp.emplace_back(i, nums[i]);
}
ranges::sort(temp, [](const pair<int,int> & p1, const pair<int,int> & p2) -> bool {
return p1.second > p2.second;
});
sort(temp.begin(), temp.begin() + k, [](const pair<int,int> & p1, const pair<int,int> & p2) -> bool {
return p1.first < p2.first;
});
vector<int> ans;
for(int i = 0; i < k; i++) {
ans.push_back(temp[i].second);
}
return ans;
}
};
堡垒机只能通过特定端口 + 授权私钥才能登录
你想干什么呢
曝光这个可恶的行为,采用 ufw 阻止 input。

一上午就在封 IP,完蛋啦,网络攻击太严重了

这篇关于 ns3 的回调写的真好,比官方教程好懂的多 😵💫 https://rainsia.github.io/2018/05/01/ns3-006/
简单双指针怎么有 2276 分?不会都和我一样刚开始不看题啪地写成区间 DP 了吧 😋。注意预处理 power。
C++
constexpr int MOD = 1e9+7;
class Solution {
public:
int numSubseq(vector<int>& nums, int target) {
vector<int> vec_pow(nums.size() + 5, 1);
for(int i = 0; i < (int)nums.size() + 4; i++) {
vec_pow[i + 1] = (vec_pow[i] << 1) % MOD;
}
ranges::sort(nums);
// middle 1 << j;
long long ans = 0;
int left = 0, right = nums.size() - 1;
while(left <= right) {
if(nums[left] + nums[right] <= target) {
ans = (ans + vec_pow[right - left]) % MOD;
left++;
} else {
right--;
}
}
return ans;
}
};
アイラちゃん 🫨这么快就被打了吗
Srotseiticar 对的,一上线就打 网络安全问题着实严重
已经跑不动了 可恶还没满 20 岁呢
步频 180 配速 4 分 30 挑战失败

アイラちゃん 19 岁升大四,这是跳了几级
アイラちゃん 🫨大四和没满 20
Srotseiticar 05 年 9 月的,很快就 20 啦 😋
アイラちゃん !!比我还小 😲
アイラちゃん 🐖那你是小登,我要代表高龄門友消灭你
Srotseiticar 我是 ldx 了你们不能这么对我
![]()
鳳 笑夢 早了一年上学(因为当时我妈想要二胎所以就把我扔去上学啦 😋),于是度过了快乐的小学时光(无人管,到处玩),虽然已经没多少记忆了但还是感觉那时候很快乐
アイラちゃん 这下必须叫你小登了😡😤
アイラちゃん 怎么比我还小一个月 不公平 ![]()
アイラちゃん lz 居然和我同岁吗🤯
我现在怎么还在大一,感觉不像同龄人🤣
神秘小维 比我还小的小弟弟居然辈分比我高什么的🥵😡
神秘小维 啊アイラちゃん还以为小维已经大二升大三了 😯
アイラちゃん 刚刚结束 xdx 生涯
要变成 zdx 了 ![]()
咦为什么辈分比你高
神秘小维 我说的辈分就是大二大三之类的
表述不清
アイラちゃん 交大也有少年班吗?
アイラちゃん 你提前三年入学了,不是一年
2025 年 6 月 30 日 星期一 天气:晴
⚠️
高温警告
未来的一周上海将出现 40 度的高温,完全被笼罩在副热带高压的魔爪下,注意防暑降温!
陕西西安注意高温情况
今天面对我司混乱的 C++ 库,已经到了无法忍受的地步。遂和学长讨论,并采用了如下方法:
- docker 拉下 ubuntu22.04 x86/64镜像。
- 镜像内进行 C++ 依赖库分析,并确定所需要的动态库 (.so) 与静态库 (.a) 的所有版本。主要集中在 grpc/protobuf 这两个在分布式中很常见但 ABI 非常混乱的库上。(对的拆动态库花了一天)
- 建立依赖图关系,通过镜像打包和编译好所需要的库。
- 提供 github actions 进行 ubuntu22.04 x86/64 库的编译,针对 arm64 架构采用 qemu 模拟编译。
- 分发完成所需容器并且提供打包脚本。
这样既满足了采用 docker/k8s 用户也满足了用户自行单机编译运行的工作。分析和拆解动态库是折磨人的工作(和 deepseek 小姐一起快看晕了),学长负责 rust 编写构建脚本和搭建 actions。最后终于完成了依赖库的构建。
明明前几天就建议手动分拆链接动态库的
经典双指针(固定左,枚举右)。
C++
class Solution {
public:
int findLHS(vector<int>& nums) {
ranges::sort(nums);
int ans = 0;
int left = 0, n = nums.size();
while(left + ans - 1 < n) {
for(int right = left + 1; right < n; right++) {
if(nums[right] - nums[left] > 1) break;
else if(nums[right] - nums[left] == 1) {
ans = max(ans, right - left + 1);
}
}
left++;
}
return ans;
}
};
小彩蛋:保加利亚 IP 尝试协议注入~

好图镇楼,晚安~
X: @haimiya_neno

2025 年 7 月 1 日 星期二 天气晴
今天需要提交 tensor 与缓存系统的交互单元测试。于是就需要用 python 和 rust 进行绑定,写一个简单的单元测试。pyO3 是很好的选择,于是就采用 pybind,python 前端直接用 io 库进行序列化后,通过 pyO3 转换成二进制数据与后端 C++ 缓存系统进行交互,然后返回。
随后将序列化的数据传递给前面写好的队列,队列通过序列化的方式经过 pyO3 传递给 rust 后端即可。rust 到 C++ 的功能已经验证过了 😋 光跑通前端 python 的 docker 服务器和客户端 + 单测配置 + 数据通过 MPSC 队列传递给后端就花掉了一整天的时间(还请求了 AI 的帮助因为 pyO3 文档太长了一下子读不完 😭

https://docs.rs/pyo3/latest/pyo3/
今天的每日一题好简单~
C++
class Solution {
public:
int possibleStringCount(string word) {
int cnt = 1;
for(int i = 1; i < word.size(); i++) {
if(word[i] == word[i-1]) {
cnt += 1;
}
}
return cnt;
}
};
rust
impl Solution {
pub fn possible_string_count(word: String) -> i32 {
let mut ans = 1;
let chars = word.into_bytes(); // Serialize;
for i in 1..chars.len() {
if chars[i - 1] == chars[i] {
ans += 1;
}
}
ans
}
}
好累,今天还加班了(本来跑通前端就好,结果下班前又来了个单测需求 😵💫)
C++
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
ListNode *pre = head, *cur = head -> next;
while(cur) {
if(cur -> val < pre -> val) {
pre -> next = cur -> next;
for(ListNode *cmp = head; cmp != pre -> next; cmp = cmp -> next) {
if(cmp == head && cmp -> val > cur -> val) {
cur -> next = cmp;
head = cur;
break;
} else if(cmp -> val <= cur -> val && cmp -> next -> val > cur -> val) {
cur -> next = cmp -> next;
cmp -> next = cur;
break;
} else continue;
}
cur = pre -> next;
} else {
pre = cur;
cur = cur -> next;
}
}
return head;
}
};
快睡觉,好困~晚安~
X: @Ly05YY

アイラちゃん 今日无图?
IO 补题中
アイラちゃん grpc/protobuf 这两个在分布式中很常见但 ABI 非常混乱的库上。
读到这里已经开始笑了
アイラちゃん 🫨一天就能搞定新需求吗
Srotseiticar 补单测而已不复杂 ![]()
今天就是开会 + 开会 + 看后端的代码,确定接下来的方向。明天就是继续完善,提供一个 docker-file,再补一下异常单测就可以了。
课题组这边也要思考怎么做仿真,怎么做逐包故障仿真。u1s1 古老的仿真工具 + c++14 及以下的标准确实有点挑战。今天就观察了一下 clos 的结构写了一个简单的拓扑生成脚本,明天再去思考怎么打流吧。
今天的题目有些难。刚开始我想的是逆元 + 组合数 + 预处理,后来发现不太行。于是就多观察了一下,发现可以正难则反,计算所有组合 - 非法组合的个数,非法组合的个数正好是一个完全背包(至少选一个)问题。第一次独立做出来 2600 分的题目还是很开心的。
C++
constexpr int MOD = 1e9+7;
class Solution {
public:
using ll = long long;
int possibleStringCount(string word, int k) {
// First pass.
if(k > word.size()) return 0;
vector<int> counts;
ll prev = 1;
int cnt = 0;
for (int i = 0; i < word.length(); i++) {
cnt++;
if (i == word.length() - 1 || word[i] != word[i + 1]) {
if (cnt > 1) {
if (k > 0) {
counts.push_back(cnt - 1);
}
prev = prev * cnt % MOD;
}
k--;
cnt = 0;
}
}
if(k <= 0) return prev;
// Using backpack method.
int n = counts.size();
vector<vector<int>>dp(n+1, vector<int>(k, 0));
ranges::fill(dp[0], 1);
vector<int> prefix_sum(k+1, 0);
for(int i = 0; i < n; i++) {
for(int j = 0; j < k; j++) {
prefix_sum[j+1] = (prefix_sum[j] + dp[i][j]) % MOD;
}
for(int j = 0; j < k; j++) {
dp[i + 1][j] = (prefix_sum[j + 1] - prefix_sum[max(j - counts[i], 0)]) % MOD;
}
}
return (prev - dp[n][k-1]+MOD) % MOD;
}
};
今天有些累,早点睡吧。
X: @kasuga_iz

Mark 一下,一个是缓存 backend,分布式 kvcache。~
https://github.com/kvcache-ai/Mooncake
https://www.usenix.org/system/files/fast25-qin.pdf
一个是 unify 通信库~
https://github.com/uccl-project/uccl
https://arxiv.org/pdf/2504.17307
io_uring 的 async 想法与目前的 RDMA 队列设计惊人地相似,也 mark 一下这篇,还是学的太少了
https://arthurchiao.art/blog/intro-to-io-uring-zh/#1-linux-io-系统调用演进
今天加班了。因为发现了一个很严重的缺陷:当项目使用 io_uring 的方式,向下方的 mooncake 缓存传递 msg_key 来获得 value 的时候,顶层的队列析构时需要显式通知下面释放内存,否则在新的 WQ 出现时,调用底层 mooncake 有可能会因为没有显式通知底层导致 memory 被 pin 住没有释放,造成写入失败。于是增加了一个单测,来 hack 掉这样的一种情况。后面再看他们如何显式通知下方 mooncake 缓存进行释放。此外修复了异步状态下获取到不正确的 key 的问题,采用版本控制的手段进行(对的就是增加版本号然后 epoll 一个个轮询)。
今天还完成了一个 dockerfile 和 dev-container.json 准备用于项目在 ubuntu20.04 和 ubuntu22.04 之间的分发。因为目前需求的节点具有两种不同的系统,因此我们的打包需要兼顾两种系统不同的 ABI 和 syscall。docker+k8s 就好了。
简单题不想多想,brute-force 就好了。
C++
class Solution {
public:
char kthCharacter(int k) {
string str;
string word = "a";
for(int i = 0; i < 9; i++){
str = word;
for(auto x : word){
str += (x + 1);
}
word = str;
}
char ans = str[k - 1];
return ans;
}
};
今天没有图了
明天再贴,因为想睡觉 😵💫
Mark: 来自 🥕 群:Introducing maple trees
https://lwn.net/Articles/845507/
アイラちゃん k < 2000, n < 5e5 感觉只能反着来了(
アイラちゃん hack!
アイラちゃん 0 ms runtime 😊
char kthCharacter(int k) {
int op = 0;
while (k > 1) {
op++;
int n = int(log2(k));
k -= int(pow(2, n)) == k ? int(pow(2, n)) / 2 : int(pow(2, n));
}
return char(op % 26 + 'a');
}
2025 年 7 月 6 日 星期日 天气:晴
今天在 tuple hash 的过程中遇到了问题。原生的 std::tuple 并不支持 hash。那么我们就自己来写一个吧,顺便学习一下相关的奇怪知识。
以下实现是作者自己的理解,可能有误,望读者批评指正!
首先我们知道,对于一个哈希的重载,std::hash 是一个结构体,我们需要重载他的 () 来实现自己的哈希函数。我们的大概想法如下:
template<typename T>
struct std::hash<T> {
std::size_t operator()(const T & arg) const {
return my_hash(arg);
}
};
那么对于一个 tuple,因为他的模板特化时模板参数不定长,我们就需要采用变长模板来实现我们的 hash 结构体重载 operator()。这样我们大概为:
template<typename ... T>
struct std::hash<std::tuple<T...>> {
std::size_t operator()(const std::tuple<T...> & arg) const {
// variadic template deal;
}
};
这样我们就需要思考如何递归地解决我们的模板展开。我们需要通过实现递归式地展开直到到达模板的末尾,通过 hash_combine 的方式来一个个组合成我们的最终 hash 值。首先我们先定义 hash_combine, 他将获得 tuple 中含有的基本类型
template <typename T>
inline void hash_combine(std::size_t& seed, const T& val) {
std::hash<T> hasher;
seed ^= hasher(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
接下来我们需要一个个地取值放入其中。我们可以通过:
template<std::size_t I = 0, typename ... T>
typename std::enable_if<(I < sizeof...(T)), void>::type
my_hash(std::size_t & seed, const std::tuple<T...> & tup) {
hash_combine(seed, std::get<I>(tup));
my_hash<I + 1, T...>(seed, tup);
}
首先学习一下 std::enable_if<B,T>。在 cppreference 上你可以看到:
If
Bis true,std::enable_ifhas a public member typedeftype, equal toT; otherwise, there is no member typedef.This metafunction is a convenient way to leverage SFINAE prior to C++20's concepts, in particular for conditionally removing functions from the candidate set based on type traits, allowing separate function overloads or specializations based on those different type traits.
当 B(表达式)为 true 的时候,我们的 std::enable_if 将会具有 public 成员类型 T, 否则将不会具有类型的定义。这样,我们的元方程就可以使用 SFINAE("Substitution Failure Is Not An Error",替代失败并不是错误) 特性(其实我还没弄懂这到底是个什么),特别是基于类型萃取来讲函数从候选集合中移除(这里涉及到重载决议),允许不同的函数重载/特化基于不同的类型萃取。
说了一大段废话,其实就是:符合 B 的时候,函数出现并返回 T 类型。否则函数不会出现(不会被重载选择)。
这样在最后我们补充一个递归终点:
template <std::size_t Index = 0, typename... Types>
inline typename std::enable_if<Index == sizeof...(Types), void>::type
my_hash(std::size_t&, const std::tuple<Types...>&) {
}
这样,在 index 没有到达模板参数长度时,我们将采取前面的函数,持续递归。否则什么都不做。
最后补充上我们的结构体。在 C++11 的标准下就可以运行。可以尝试使用查看:
compiler explorer 1 C++-11 version
compiler explorer 2 boost mimic
C++-11 Version
template <typename T>
inline void hash_combine(std::size_t& seed, const T& val) {
std::hash<T> hasher;
seed ^= hasher(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template <std::size_t Index = 0, typename... Types>
inline typename std::enable_if<Index == sizeof...(Types), void>::type
my_hash(std::size_t&, const std::tuple<Types...>&) {
}
template<std::size_t I = 0, typename ... T>
typename std::enable_if<(I < sizeof...(T)), void>::type
my_hash(std::size_t & seed, const std::tuple<T...> & tup) {
hash_combine(seed, std::get<I>(tup));
my_hash<I + 1, T...>(seed, tup);
}
template <typename ... T>
struct std::hash<std::tuple<T...> > {
std::size_t operator()(const tuple<T...> & tup) const {
std::size_t seed = 0;
my_hash(seed, tup);
return seed;
}
};
实际上这已经很接近我们 boost 库的实现,在 boost 库中,我们的 enable_if 采用的是通过 std::tuple_size<T>::size 来实现。
Boost Implementation(戏仿)
template<class T>
void hash_combine(std::size_t & seed, T element) {
std::hash<T> hasher;
seed ^= hasher(element) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template<std::size_t I, typename T>
typename std::enable_if<(I == std::tuple_size<T>::value), void>::type
hash_combine_tuple_like(std::size_t &, T const &)
{
}
template<std::size_t I, typename T>
typename std::enable_if<(I < std::tuple_size<T>::value), void>::type
hash_combine_tuple_like(std::size_t & seed, T const & v)
{
hash_combine(seed, std::get<I>(v));
hash_combine_tuple_like<I+1>(seed, v);
}
template <class T>
std::size_t hash_tuple(T const & v) {
std::size_t seed = 0;
hash_combine_tuple_like<0>(seed, v);
return seed;
}
template <class T>
struct hash_tup {
std::size_t operator()(T const & val) const {
return hash_tuple<T>(val);
}
};
参考资料: boost container.hash
挖坑:后面再学习闭包,闭包展开,折叠表达式等相关技巧。
今日每日一题:
C++
class FindSumPairs {
private:
vector<int> nums1, nums2;
unordered_map<int, int> cnt;
public:
FindSumPairs(vector<int>& nums1, vector<int>& nums2) {
this->nums1 = nums1;
this->nums2 = nums2;
for (int num: nums2) {
++cnt[num];
}
}
void add(int index, int val) {
cnt[nums2[index]]--;
nums2[index] += val;
cnt[nums2[index]]++;
}
int count(int tot) {
int ans = 0;
for (int num: nums1) {
int res = tot - num;
if (cnt.count(res)) {
ans += cnt[res];
}
}
return ans;
}
};
アイラちゃん 斯~挂梯子才能上,我还以为维护呢试了两天,主动屏蔽国内了吗
アイラちゃん 主动屏蔽国内了吗
看样子是的🥺
往好处想 或许没那么容易被时间了 可以更好地防盒()
长恨此身非我有 何时忘却营营
“We are all in the gutter, but some of us are looking at the stars.”
晚安,塬友们。我们终将一步步,走向属于我们自己的彼岸。
アイラちゃん std::make_index_sequence !
Zerick 先挖坑~后面再补 😋
最近实在太忙,实习单位写一个调用 etcd 的分布式 kv 框架支持(pybind+C++),以及配套的单元测试,刚刚交付成功,后面还要更改 lease 问题和 ttl 问题(过期丢弃),以及考虑一下通信失败的重传问题。学校这边也要我修改 ns3 仿真支持包流混合测试场景,非常非常忙碌。我会尽力在周末的时候更新动态 😵💫
アイラちゃん 唉不行了我想多看一会土塬但是太困了,晚安塬友们
今天赶网络仿真的需求,在无数次从一堆的静态反射和回调的乱麻中改了整整一天,终于将策略信息附加到报文头部上了。
实习那边又来了两个新的需求,主要是消费者消费进度持久性保证和生产者产出信息消费持久性保证。这两个并不难,通过 etcd 存储一个简单的 map 就好。
本周算是悲喜交加。喜的是本周的 leetcode 都可以顺利做出,在课题组和实习企业的代码产出都很稳定,能够勉强应付需求。后面还是要多磨练自己的技术。悲的是夏令营几乎被拒完了,未来迷茫且几乎已成败局。
这就是 confusion 和 disorder 吧。
好困啊感觉要睡了,逛逛土塬就赶紧睡了,有些累。 🥺


虽然有点晚了,但是需要将重心转移到考研上了。其实大三下也在断断续续地准备过,不过现在是要开始准备了。
从下周开始就只能一周一记了,每一周会抽出一段时间来记录这一周的实习经历,学习到的知识和复习的进度。感觉自己就像玩 Buckshot Roulette 的最后一局一样,四发空包弹,四发实心弹,和未来这个庄家仅有 5 条命的最后一局了。是死是活呢?
目前来看复习 11408 的进度是
- 数学一(一轮)
- 高等数学:多元微积分
- 线性代数:未开始
- 概率论和数理统计:未开始
- 英语一(单词已经几乎熟悉)
- 政治一(10 月份再开始都不迟)
- 专业课:
- 均没有开始
有点麻烦啊 🥺
上周日就是黄金了吗,太忙了都没注意到
😍
转瞬间我也是老用户了
介绍一下近况:最近这一个周感觉很累是因为正在三线作战 (leetcode+ 考研 + 实习)。每天至少一道的 leetcode 保持手感 + 负责了即将用于分布式训练系统的多生产者单消费者的一个中间部件的需求。所以每天不是 debug 就是在 debug 的路上,还需要根据训练团队的需求来更改。
在 profile 的过程中,我们发现消费者在选择生产者还是轮询的方式要比信号量更好,原因猜测是因为目前并不需要数据按序传输给消费者进行训练,这样信号量就无法做到真正意义上的“每个 segment 都有数据通过生产者生产出来传递给消费者进行训练”。
不过目前已经基本完成了,但是实际上都是调库,对具体的原理还需要去学习和研究才行。
アイラちゃん 上周日就是黄金了吗
🔫
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 😱这段话也有🔫要素吗
Srotseiticar 土源的黄金对标水源的活跃用户 + 博物学者,于是🔫
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 判处
🔫
立即执行 😋
アイラちゃん 🔫
已经不是一般的活跃用户了!😡
1s
🔫 🔫 🔫 🔫 🔫 🔫
执法有温度,枪枪有准度
致敬全小将
本周体验:累+down

https://mp.weixin.qq.com/s/vQ6u3PvDW_4pYNN5Q5_TRg
图片灵感来源↑
Leetcode 每日至少一题
复习到无穷级数 仅完成到微分方程
看完 Rust 的 Ownership Chapter4 仅完成到 Chapter 3
完成实习的小项目
Plans
下个周计划
复习完无穷级数
看完 Rust 的第四章
学习一下异步编程
Leetcode 每日至少一题
开始做王道的数据结构题目
学习 eBPF,准备为 benchmark 做准备
希望下周不出 bug 每天都能准时下班
出了 bug 我也要准时下班,毕竟是实习生嘛
アイラちゃん 希望下周不出 bug 每天都能准时下班
你不要过来 😭
🔫
没想到明天竟然是
0721
😋
アイラちゃん 也是柚子社最爱的日期之一😋

アイラちゃん 出了 bug 我也要准时下班,毕竟是实习生嘛
对的对的
周末快乐呀
本周主要是将组里的 rust 缓存项目通过 pyO3 进行打包,但是有一些基础库是只能在 manylinux2014 里面编译的,实在是有些痛苦。刚开始的时候进行编译是全量编译,即修改一点文件就需要将所有的文件全部重新编译。后来呢用 python 写了一个简单的脚本来判断哪些文件发生了变化(数据挖掘的知识竟然用上了),对代码文件分块读取哈希(MD5)的方式来判断哪些 rust 文件发生了变化,最后进行增量编译,让项目的编译时间减少了~
随后另一位同学进行了简单的 profile 发现性能损耗十分严重。经过分析发现是缓存系统内 p2p 的通信开销很大,不太明白为什么基于 SPDY 的一个 p2p 的库会慢。后来 leader 就更换了一个 libp2p 的东西(基于 QUIC 的)就大幅提高了性能!可以参见这篇文章为什么 QUIC 优于 SPDY。
简单来说:SPDY 是在应用层上工作的。而 QUIC 是在传输层上工作的。SPDY 会遭到以下的损失:
- 简单的单个包丢失将会导致严重的流头阻问题。单个 SPDY 连接将会在 TCP 层上连接上复用多个数据流(Streams)。然而,TCP 本身只提供一个单一的、有序的字节流接口。这样一个丢包会引起多个 SPDY 流被阻塞。
- 受到拥塞控制的严重影响,我们可以发现,每个丢包将影响一个流的拥塞窗口。这样一个 TCP 连接在出现丢包后,整个 SPDY 流将会立刻减少 50% 的带宽 (Reno),而实际上多个 TCP 连接应该互不影响。SPDY 无法隔离其抽象出来的多个流。
- TLS 作为安全性的体现,将增加一个 RTT 时间,然而这并不是出于安全性需求,而是 TLS 的固有部署,这样造成冗余的同时增加延时。
- TLS 的历史遗留问题,需要顺序化解密我们的包序列。
于是 QUIC 横空出世,在 UDP 上进行部署,简单修改后成为了在传输层运行的多路无复用的流,并针对 UDP 提供了自己的拥塞控制。换了一个库后就从 600ms 提升到了 50ms,接近 1/10 的提速比!
复习完无穷级数 复习到一半
看完 Rust 的第四章
学习一下异步编程 学习到一半 QAQ
Leetcode 每日至少一题
开始做王道的数据结构题目
学习 eBPF,准备为 benchmark 做准备 优化构建去了 😋
X: @ji10me

异步好难啊 QAQ
紧急加更
堪察加半岛 M8.8 强震,是类似于 2011 年 3 月 11 日东日本大地震 M9.1 的浅层逆冲断层地震。深度 21.5km。

我们可以从下图中了解到这条经典断层上的强烈地震。

目前可以看到,来自美国中途岛的数据,距离震中差不多 3300km 处录的接近 3m 的浪高。


今天上午 7 时 24 分,自然资源部海啸预警中心发布海啸黄色警报,具体如下。

目前海啸警报已经解除。
今天上午 9 时 40 分(北京时间 8 时 40 分),日本气象厅对北海道太平洋沿岸和本州岛太平洋沿岸大部发布海啸警报,其余部份地方发布海啸注意报。

自然资源部相关预测和浪高分布图:


一直实习 + 实验室打工太累了,今天不管说什么都不做事了。下午结束了 oncall 的 bug 后就去看电影了。

南京照相馆。
山河喋血,生灵涂炭。尸横遍野。真实的,1937 年 12 月 13 日后的南京,赤裸裸地展现在观众面前。
除了某些比较突兀的情节以外,导演非常成功地展现了当时日军的残暴和兽行。最令人印象深刻的就是(谨防剧透,模糊处理) 伊藤秀夫宁愿将饭团喂给狗,也没有正眼看过旁边堆积如山的尸体,将他的伪善和做作展现的淋漓尽致。
正视历史,珍惜和平。时间冲刷不掉隐藏在阴暗处的罪恶,历史也将永远记得 19371213,永不忘南京之殇。
开发教训记录:
- 当一个类唤起的子线程没有通过 event/semaphore 终止时,将不会触发
__del__()进行析构。这样如果将事件/信号量防止在析构函数中将会导致线程无法 join。- 解决方法:显式调用
__del__()函数或者将线程更改为守护线程。- 在子进程只有守护进程的时候将会调用
__del__()函数从而退出。- 很显然我们更倾向于 sol-2。用户并不可能喜欢自行进行资源管理。
Error Version Python: Never Exit
import threading, time, multiprocessing class test(): def __init__(self): self.stop_flag = threading.Event() self.background = threading.Thread(target = self.run) self.background.start() def run(self): while self.stop_flag.is_set() == False: print("Alive") time.sleep(2) def __del__(self): self.stop_flag.set() self.background.join() def dosomething(): test_obj = test() for _ in range(5): time.sleep(1) if __name__ == '__main__': multiprocessing.set_start_method('spawn') process = multiprocessing.Process(target = dosomething) process.start() process.join()Sol-1
import threading, time, multiprocessing class test(): def __init__(self): self.stop_flag = threading.Event() self.background = threading.Thread(target = self.run) self.background.start() def run(self): while self.stop_flag.is_set() == False: print("Alive") time.sleep(2) def __del__(self): self.stop_flag.set() self.background.join() def dosomething(): test_obj = test() for _ in range(5): time.sleep(1) test_obj.__del__() if __name__ == '__main__': multiprocessing.set_start_method('spawn') process = multiprocessing.Process(target = dosomething) process.start() process.join()Sol-2
向社区的大家抱歉,这篇 post 有问题,我会找时间重新改正,本质上是对 python 的 gc 回收机制,强弱引用的理解不透彻导致的 😭
不过门友可以先猜猜问题在哪里 😋
2025 年 9 月 14 日 星期日
失踪人口回归~
实在实在是太忙了,忙晕了。好久没有更新了。
今天是アイラちゃん的生日,祝自己生日快乐。
本以为自己不可能拿到推免名额了,结果在 9 月 8 日下午的时候突然被通知拥有了推免名额,曾经不抱希望报名的南京大学智能科学与技术竟然也给了面试名额,非常惊人。。。
于是就跑到苏州参加了预推免,分笔试,机试和面试。笔试和机试个人感觉都 AK 了,因为题目并不难(最难的就是有一个二维 dp 而已。。。)但是面试感觉老师们对アイラちゃん不感兴趣(可能是因为我没有像样的论文/大创项目),只有对校内操作系统的改进和改造,开源项目(mooncake)的贡献和一个强化学习比赛项目,所以英文介绍也被打断,项目介绍也被打断,跟我说了谢谢就让我走了。(顺带一提アイラちゃん是倒数第二位被面试的。。。)
报名了集成电路学院的工程硕博士预推免,也过了,但是现在还没有发具体面试地点的邮件,是不是没有确认参与复试。。。有点担心
希望アイラちゃん能找到能够接受的接收单位(硕士) 😭 也祝大家心想事成~
アイラちゃん Bon anniversaire!
好想盒盒 lz 不过我这种终极 i 人估计如果别人邀请我开盒我第一个害怕逃遁()
长恨此身非我有 何时忘却营营
アイラちゃん 生日快乐!
アイラちゃん 生日快乐!
祝顺利~
本校工程硕博看起来像是把我默拒了。
南京大学没有消息。估计也是默拒了。
原来边缘人 + 三无选手(无项目,无论文,无竞赛)就是死局呀。
那算了,名额浪费就浪费吧,收拾一下明天重新准备考研吧。向下保是没有意义的,读博=赌博,アイラちゃん断然没有那个能力去。
签署了承诺书,直到 2026 年 10 月 30 日前都不能参与就业,只有一条路走了。
虽然很难受但是也不能裹足不前呀。我永远都会记得上海交通大学,感谢这所大学给我三年带来的所有痛苦。
アイラちゃん 可以考虑去西安交通太学
アイラちゃん 摸摸楼主
签署了承诺书,直到 2026 年 10 月 30 日前都不能参与就业,只有一条路走了。
🫢还有这种东西的吗
アイラちゃん 签署了承诺书
这个是什么啊🥺
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 实习竞业协议
紫金港第一皮革<・)))><< 
アイラちゃん 感觉对我来说选择考研比选择赌博更需要勇气(
所以就去报了一些直博项目了,虽然现在看来当时选硕士以我的笔试面试发挥应该至少是专硕
不过已经选择了,落子无悔吧
第一次感觉有点累了,可能之前没有太拼命吧,谢谢大家的关心。让我自己休息一会儿吧。
打算开一些新的坑,随便玩一玩了
板子刚刚到货,想学习一下 51 单片机
目前参考的教材是这本:


争取在明年春天前不把这个板子烧坏 🤣
アイラちゃん 6 年前我买了一个跟你这个一模一样的板子,普中 C51。不过感觉倒不如直接用 STM32,或者 TI DSP。
IO 嗯,打算看完 51 后有时间就去玩 STM
最后兜兜转转跨保到集成电路工程硕士了,这下不得不补很多专业知识了...
今天就这样吧,后面要补上的知识还有很多
Today Learning
51 单片机学习:初识 51
参考书籍:《新概念 51 单片机 C 语言教程入门提高开发拓展全攻略第 2 版》,作者:郭天祥
芯片初识
自己买的单片机是 STC89C52RC 40IPDIP40. 可以在下图中看到拓印在芯片上的标号。

相关名称的解释如下:
- STC:前缀,表示芯片 STC 公司生产的产品。其他前缀还有如 AT、i、Winbond、SST 等。
- 8:表示该芯片为 8051 内核芯片。
- 9:表示内部含 Flash E2PROM 存储器。另,80C51 中的 0 表示内部含 Mask ROM(掩模 ROM)存储器,87C51 中的 7 表示内部含 EPROM 存储器(紫外线可擦除 ROM)。
- C:表示该器件 CMOS 产品。另,89LV52 和 89LE58 中的 LV 和 LE 表示该芯片为低电压产品(通常为 3.3V 电压供电);89S52 中的 S 表示该芯片含有可串行下载功能的 Flash 存储器,即具有 ISP 可在线编程功能。
- 5:固定不变。
- 2:表示该芯片内部程序存储空间的大小,1 为 4KB,2 为 8KB,3 为 12KB,即该数乘上 4KB 就是该芯片内部的程序存储空间大小。程序空间大小决定了一个芯片所能装入执行代码的多少。我们的芯片是 8KB。
- RC:STC 单片机内部 RAM(随机读写存储器)为 512B。另,RD+ 表示内部 RAM 为 1280 B。
- 40:表示芯片外部晶振最高可接入 40MHz。对于 AT 单片机,其值一般为 24,表示其外部晶振最高为 24MHz。
- C:产品级别,代表商业级,表示芯片使用温度范围,为 0°C~+70°C。
- PDIP—产品封装型号,表示双列直插式。
2523:表示本批芯片生产日期为 2025 年第 23 周。
HBME70.X90C 不详(有关资料显示,此标号表示芯片制造工艺或处理工艺)。
这样我们就有了一颗 8051 内核,含有 Flash E2PROM 存储器,CMOS,8KB 内存,最高晶振为 40KHZ,生产于 2025 年第 23 周的芯片,使用温度在 0~70 摄氏度之间。
我们可以看到我们的芯片是 DIP 双列直插式封装的,其他的还有 PLCC(带引线的塑料芯片封装),QFP(塑料方型扁平式封装),PGA(插针网络阵列封装),BGA(球栅阵列封装等),此处按下不表。
引脚
我们的单片机是 40 脚 DIP 封装的,也有 20,28,32,44 等。我们以我们的实际图来认识引脚。
- 首先识别到半圆形的凹槽,这个是我们引脚的起点。通过逆时针的方式,我们的标号如下:

我们将引脚可以分成 3 类:
-
电源与时钟引脚。VCC、GND、XTAL1、XTAL2
-
变成控制引脚。RST、、ALE/、/VPP。
-
I/O 口引脚。例如 P0~P3,四组八位 I/O 口。
详细了解
-
(40 脚)、(20 脚)单片机电源引脚,不同型号单片机接入对应电压电源,常压 5V,低压 3.3V。我们的芯片是 5V——3.3V。
-
XTAL1(19 脚),XTAL2(18 脚)外接时钟引脚。前者是输入端,后者是输出端。
-
8051 时钟有两种方式:(1)片内时钟振荡。在两脚外接石英晶体与 10~30pF 的振荡电容。(2)外部时钟:输入端接地,输出端接入外部时钟。
-
RST(9 脚)复位引脚。
国庆后给一个 TODO List:
学习《信号与系统》
学习《数字信号处理》
复习《线性代数》
学习《数学物理方法》
学习简单的嵌入式编程(51 单片机,STM)
希望能够在新的一年到来前实现补齐这些基础知识。除此之外,未来想做的 todo list 还有很多很多,给一个长长的,很难完成的 todo-list:
Rustlings:想要学习新的语言
gopl: 学习 golang
15445: 学会 Rust 后尝试重构这个实验
6.824: 用 golang 写一下分布式系统
cs144: 写一个 TCP/IP
先这样吧,感觉缺失的知识很多。。。也不知道自己的毅力能否支持自己完成... 感觉自从过了这个暑假,失去了很多心气,变得很累,还有一种颓丧感。
希望能够靠不断更新自己的 blog 和自己的楼来进步吧。
路漫漫其修远兮,吾将上下而求索。加油吧。
除此之外,Leetcode 我也会坚持继续,希望能够成功达成 365 挑战。
Leetcode

不过这个暑假还是有一点收获的
给 mooncake 做了一个 CI/CD 的 PR,他们的动态库在打包的时候没有移除干净,对于 python3.8 的 patchelf,无法移除具有通配符的动态库文件,这样 python3.8 在使用该库的时候可能会污染本地环境,带入不必要的动态库。
也算是尽自己的一点微不足道的“螺丝钉”能力赚了一个 PR(跟白捡似的
)
アイラちゃん 楼主好强 🥹
主治医师李大华 我不行的,不然也不会这么累
只有拿烂命狠干才能达到普通水平 🥺
アイラちゃん 加油呀,树莓是 15445 摆子,没有突击掉现在烂尾了 🫠
Hard time comes again and never leave.
RaspberryPi 谢谢树莓派老师
一起加油!
今天学习数学物理方法到解析函数,随后一直摸鱼
发现没有 ddl 自己就会懒下来。。。

继续恢复训练,今天是 4km/5min 配速,希望能慢慢恢复到大二学年的体能状态。。。
为什么要先跑一圈外圈呢,因为跑道上人很多,需要稳定自己的速度后再慢慢切到内圈。。。
最后抱怨一句:亲爱的上海市,您还要热到什么时候呢 😇
アイラちゃん 发现没有 ddl 自己就会懒下来。。。
有 ddl 的某皮革鱼也会懒下来()
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 欢迎加入
🐷
大家庭~
アイラちゃん 西交的操场现在还没修好(
施工告示写着 9.20 竣工但是感觉十一结束前修不好了
Zerick 只是十一吗 那很好了
上交动不动封桥洞(东西区通勤的交通要道)而且开放的时间经常比预估时间迟好多
还我三号桥洞😭😭😭
长恨此身非我有 何时忘却营营
因为今天的题目很有意思所以特别想跟大家分享一下。
一开始我想到了凸包,然后想到凸包后可以采用 的渐进算法算出最大面积。但是灵神的回答中提到了一篇论文!
maximal-area-triangles-in-a-convex-polygon-4k6l0fx7vg(1).pdf|attachment (2.5 MB)
作者声称我们可以通过 的算法复杂度找到这个最大值。
于是我就写出了下面的这一大段💩山代码
C++
class Solution {
public:
double largestTriangleArea(vector<vector<int>> &points) {
int n = points.size();
int *stack = new int[n + 2];
memset(stack, -1, sizeof(int) * (n + 2));
bool *used = new bool[n];
memset(used, false, sizeof(bool) * n);
ranges::sort(points, {}, [](const vector<int> &p) -> tuple<const int &, const int &> {
return tie(p[0], p[1]);
});
// lower convex hull.
int stack_pt = 0;
for (int index = 0; index < (int)points.size(); index++) {
int x = points[index][0], y = points[index][1];
// When \vec{StSt-1 x StP <= 0} and more than 2 elements in stack: pop
while (stack_pt + 1 > 2) {
int top1 = stack[stack_pt], top2 = stack[stack_pt - 1];
int x1 = points[top1][0], y1 = points[top1][1];
int x2 = points[top2][0], y2 = points[top2][1];
if ((x1 - x2) * (y - y1) - (y1 - y2) * (x - x1) >= 0) {
used[stack[stack_pt]] = false;
stack_pt--;
continue;
} else {
break;
}
}
if (used[index] == false) {
stack[++stack_pt] = index;
used[index] = true;
continue;
}
}
// upper convex hull.
int lower_size = stack_pt;
for (int index = (int)points.size() - 1; index >= 0; index--) {
int x = points[index][0], y = points[index][1];
// When \vec{StSt-1 x StP <= 0} and more than 2 elements in stack: pop
while (stack_pt > lower_size) {
int top1 = stack[stack_pt], top2 = stack[stack_pt - 1];
int x1 = points[top1][0], y1 = points[top1][1];
int x2 = points[top2][0], y2 = points[top2][1];
if ((x1 - x2) * (y - y1) - (y1 - y2) * (x - x1) >= 0) {
used[stack[stack_pt]] = false;
stack_pt--;
continue;
} else {
break;
}
}
if (used[index] == false) {
stack[++stack_pt] = index;
used[index] = true;
continue;
}
}
// Calculate the triangle by calculate the convex hull point.
auto S = [](int x1, int y1, int x2, int y2, int x3, int y3) -> double {
return 0.5 * abs(x1 * y2 + x2 * y3 + x3 * y1 - x1 * y3 - x2 * y1 - x3 * y2);
};
auto dist = [&](int a, int b, int c) {
int xa = points[stack[a]][0], ya = points[stack[a]][1];
int xb = points[stack[b]][0], yb = points[stack[b]][1];
int xc = points[stack[c]][0], yc = points[stack[c]][1];
return (yb - ya) * xc + (xa - xb) * yc;
};
auto area = [&](int a, int b, int c) -> double {
int x1 = points[stack[a]][0], y1 = points[stack[a]][1];
int x2 = points[stack[b]][0], y2 = points[stack[b]][1];
int x3 = points[stack[c]][0], y3 = points[stack[c]][1];
return 0.5 * abs(x1 * y2 + x2 * y3 + x3 * y1 - x1 * y3 - x2 * y1 - x3 * y2);
};
double ans = 0;
stack[0] = stack[stack_pt], stack[stack_pt + 1] = stack[1];
if (stack_pt == 3) {
int x1 = points[stack[0]][0], y1 = points[stack[0]][1];
int x2 = points[stack[1]][0], y2 = points[stack[1]][1];
int x3 = points[stack[2]][0], y3 = points[stack[2]][1];
return S(x1, y1, x2, y2, x3, y3);
}
// Find one 3 stable.
auto findOne3Stable = [&]() -> tuple<int, int, int> {
double max_S = 0.0;
int a = 1, b = 2, c = 3;
int C = c;
for (int B = b; B < stack_pt; B++) {
if (C == B) C++;
while (C < stack_pt && dist(1, B, C + 1) > dist(1, B, C) + 1e-6) {
C++;
}
double new_area = area(1, B, C);
if (new_area > max_S) {
max_S = new_area;
a = 1;
b = B;
c = C;
}
}
// Step 2: find one 2-stable.
while (dist(b, c, a + 1) > dist(b, c, a) + 1e-6) {
a = a % stack_pt + 1;
bool flag = true;
while (flag == true) {
flag = false;
while (dist(c, a, b + 1) > dist(c, a, b) + 1e-6) {
b = b % stack_pt + 1;
flag = true;
}
while (dist(a, b, c + 1) > dist(a, b, c) + 1e-6) {
c = c % stack_pt + 1;
flag = true;
}
}
}
// Step 3: find one 3-stable.
while (dist(b, c, a - 1) > dist(b, c, a) + 1e-6) {
a--;
if (a == 0) a = stack_pt;
bool flag = true;
while (flag == true) {
flag = false;
while (dist(c, a, b - 1) > dist(c, a, b) + 1e-6) {
b--;
if (b == 0) b = stack_pt;
flag = true;
}
while (dist(a, b, c - 1) > dist(a, b, c) + 1e-6) {
c--;
if (c == 0) c = stack_pt;
flag = true;
}
}
}
return {a, b, c};
};
auto RotateAndKill = [&](int a, int b, int c) -> double {
int ans_a = 1, ans_b = 2, ans_c = 3;
double max_a = 0.0;
int r = a, t = c;
// Section 4:
while (b != t || c != r) {
// repeat 1. find A.
while (dist(b, c, a + 1) > dist(b, c, a)) {
a = a % stack_pt + 1;
}
double new_area = area(a, b, c);
// output the 3-stable.
if (new_area > max_a) {
max_a = new_area;
ans_a = a, ans_b = b, ans_c = c;
}
// Repeat 2: Calculate Ib,c.
double dx1 = points[stack[b]][0] - points[stack[b + 1]][0], dy1 = points[stack[b + 1]][1] - points[stack[b]][1];
double dx2 = points[stack[c]][0] - points[stack[c + 1]][0], dy2 = points[stack[c + 1]][1] - points[stack[c]][1];
double delta = dy1 * dx2 - dy2 * dx1;
if (delta >= -1e-9)
b = b % stack_pt + 1;
else {
double U = dy1 * points[stack[c]][0] + dx1 * points[stack[c]][1];
double V = dy2 * points[stack[b]][0] + dx2 * points[stack[b]][1];
double Ix = (U * dx2 - V * dx1) / delta;
double Iy = (V * dy1 - U * dy2) / delta;
// (yc - yb) * xa + (xb - xc) * ya;
if (dist(b, c, a) > (points[stack[c]][1] - points[stack[b]][1]) * Ix + (points[stack[b]][0] - points[stack[c]][0]) * Iy) {
c = c % stack_pt + 1;
} else {
b = b % stack_pt + 1;
}
}
}
a = ans_a, b = ans_b, c = ans_c;
return max_a;
};
// Step 1:
auto [a, b, c] = findOne3Stable();
ans = RotateAndKill(a, b, c);
return ans;
}
};
我们姑且不管数学方面是否正确,我们来尝试部署一下这个算法。
前置知识
- stable:对于一个已经提供了凸多边形点集的三角形子集 ABC,A 是 stable 的,当且仅当在固定了 BC 后,A 距离 BC 的距离是最大的。
- 我们所有的点将是顺时针的。即凸包上的点按照顺时针进行编号。
接下来我们来看算法是如何实现的。
- 找到一个 3-stable 三角形。也就是说我们首先要找到一个三角形 ABC,满足 A,B,C 都是 stable 的条件。
- 固定一个 A,然后寻找 B,C。这样我们就可以得到一个以 A 为根,B,C stable 的三角形。我们通过枚举 B,判断 C 是否是 stable 的。因为每次枚举 B,我们无需将 C 从头开始枚举,只需要从上一次的点继续向下枚举。因此摊还分析为 O(1) 平均操作(这一部分可以在官解上看到相同的做法)。对于 B 的枚举就是 。于是我们有:
findOne3stable - part1
double max_S = 0.0;
int a = 1, b = 2, c = 3;
int C = c;
for (int B = b; B < stack_pt; B++) {
if (C == B) C++;
while (C < stack_pt && dist(1, B, C + 1) > dist(1, B, C) + 1e-6) {
C++;
}
double new_area = area(1, B, C);
if (new_area > max_S) {
max_S = new_area;
a = 1;
b = B;
c = C;
}
}
- 接着,作者假设了这个 A 并不是一个 stable 点。因此需要继续进行枚举,不断枚举根节点 A 来判断这个点是不是 stable 的。这里用了一个小 trick。我们的 stack 记录在 [1...stack_pt] 中,向外延拓两点:
stack[0] = stack[stack_pt]以及stack[stack_pt+1]=stack[1]来规避掉错误的地址访问。这样我们可以尝试讨论 A 使它成为 stable 点。我们从两个方向来分别判断它是不是稳定的- 顺时针:观察 A 顺时针方向的下一个节点 A+1到达 BC 的距离是否更大。如果更大说明 A 不稳定,A <- A+1
- 逆时针:观察A 逆时针方向的下一个节点 A-1到达 BC 的距离是否更大。如果更大说明 A 不稳定。A <- A-1
这里距离可以直接采用化简后的叉乘公式。我们可以回顾一下。这里我们统一为顺时针。固定住 AB,我们有:
叉乘化简

这样可以让数值更加稳定。
findOne3Stable - part2
// Step 2: find one 2-stable.
while (dist(b, c, a + 1) > dist(b, c, a) + 1e-6) {
a = a % stack_pt + 1;
bool flag = true;
while (flag == true) {
flag = false;
while (dist(c, a, b + 1) > dist(c, a, b) + 1e-6) {
b = b % stack_pt + 1;
flag = true;
}
while (dist(a, b, c + 1) > dist(a, b, c) + 1e-6) {
c = c % stack_pt + 1;
flag = true;
}
}
}
// Step 3: find one 3-stable.
while (dist(b, c, a - 1) > dist(b, c, a) + 1e-6) {
a--;
if (a == 0) a = stack_pt;
bool flag = true;
while (flag == true) {
flag = false;
while (dist(c, a, b - 1) > dist(c, a, b) + 1e-6) {
b--;
if (b == 0) b = stack_pt;
flag = true;
}
while (dist(a, b, c - 1) > dist(a, b, c) + 1e-6) {
c--;
if (c == 0) c = stack_pt;
flag = true;
}
}
}
这样就找到一个 3-Stable 了。
接下来进入到寻找所有的 stable 三角形。作者的做法是:

最关键的部分是。我们发现 的选取方式是如下图所示的:

- 对于 C 点,做一条射线与 B 点和 B 的顺时针下一个点 B+1 的方向相反。
- 对于 B 点,做一条射线与 C 点和 C 的顺时针下一个点 C+1 的方向相同。
射线的交点就是我们的 。这里的交点怎么求取呢?
- 平行的时候, 与 叉乘为 0.我们将交点 与 BC 之间的距离定为无穷大。
- 根据平行叉乘为 0,以及克莱默公式有:

- 通过面积最大值来得到 3-stable 三角形。
于是我们有:
RotateAndKill
auto RotateAndKill = [&](int a, int b, int c) -> double {
int ans_a = 1, ans_b = 2, ans_c = 3;
double max_a = 0.0;
int r = a, t = c;
// Section 4:
while (b != t || c != r) {
// repeat 1. find A.
while (dist(b, c, a + 1) > dist(b, c, a)) {
a = a % stack_pt + 1;
}
double new_area = area(a, b, c);
// output the 3-stable.
if (new_area > max_a) {
max_a = new_area;
ans_a = a, ans_b = b, ans_c = c;
}
// Repeat 2: Calculate Ib,c.
double dx1 = points[stack[b]][0] - points[stack[b + 1]][0], dy1 = points[stack[b + 1]][1] - points[stack[b]][1];
double dx2 = points[stack[c]][0] - points[stack[c + 1]][0], dy2 = points[stack[c + 1]][1] - points[stack[c]][1];
double delta = dy1 * dx2 - dy2 * dx1;
if (delta >= -1e-9)
b = b % stack_pt + 1;
else {
double U = dy1 * points[stack[c]][0] + dx1 * points[stack[c]][1];
double V = dy2 * points[stack[b]][0] + dx2 * points[stack[b]][1];
double Ix = (U * dx2 - V * dx1) / delta;
double Iy = (V * dy1 - U * dy2) / delta;
// (yc - yb) * xa + (xb - xc) * ya;
if (dist(b, c, a) > (points[stack[c]][1] - points[stack[b]][1]) * Ix + (points[stack[b]][0] - points[stack[c]][0]) * Iy) {
c = c % stack_pt + 1;
} else {
b = b % stack_pt + 1;
}
}
}
a = ans_a, b = ans_b, c = ans_c;
return max_a;
};
真的很有意思。花了我接近 6 个小时。
アイラちゃん 好奇那个笔记是 lz 手写的吗
好漂亮的卷面🥰
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 手写的~因为几何部分手写比打 latex 快很多
アイラちゃん 羡慕这样的卷面 看着真的赏心悦目😻
长恨此身非我有 何时忘却营营

CGTN 这稿子写的好呀,学习了~
"任何关心世界局势的人都会想问:为何我们人类在历经磨难后,不能秉持更大的良知与理性,以善意相待,和平共处?"李说道。
"面对人道灾难等不堪事件,我们怎能对公然践踏公平正义的暴行视而不见、袖手旁观?面对肆无忌惮的霸权霸凌行径,我们怎能因畏惧强权而沉默顺从?"李诘问道。
"我们又怎能任由联合国创始先辈们的热血丹心,就这样湮没于历史书页?"李补充道。
アイラちゃん 最佩服的就是数学好的人 🥹
アイラちゃん typo: cramer's rule (
我们从两个方向来分别判断它是不是稳定的
- 顺时针:要求 A 顺时针方向的下一个节点 A+1到达 BC 的距离最大。
- 逆时针:要求A 逆时针方向的下一个节点 A-1到达 BC 的距离最大。
然后昨天看群友讨论的时候我看到拿任意两个点去构造 IJK 的时候完全看不懂了,遂放弃
Zerick 对的,如果 A 是稳定的那么他肯定是极大值,A+1 到 BC 和 A-1 到 BC 的距离都比他小
- 顺时针:观察 A 顺时针方向的下一个节点 A+1到达 BC 的距离是否更大。如果更大说明 A 不稳定。A <- A+1
- 逆时针:观察A 逆时针方向的下一个节点 A-1到达 BC 的距离是否更大。如果更大说明 A 不稳定。A <- A-1
更改了一下说法
2025 年 9 月 29 日
今天 deepseek 发布了 v3.2 试验版。论文很短,只有 6 页。因此アイラちゃん窃读之,并向塬友们略作分享。愿塬友们批评指正!
更好的阅读体验:
https://www.cnblogs.com/mumujun12345/p/19119720
节前发版:Deepseek v3.2 exp 之打工人的悲鸣
加班快乐...
架构
与 Deepseek-V3.1 相比,新一般的架构更改仅仅在后续训练中引入了新的稀疏注意力机制 DSA。
DSA:deepseek 稀疏注意力
主要包括两个部分:一个 ligtning indexer(索引器)和一个细粒度的 token 选择机制。
Lightning indexer
Step 1: 计算索引分数。
计算了 当前询问 Q token 与一个 前序 token 的索引分数,决定了 Qtoken 将会选择哪一个 token。
其中我们有:
-
索引头的数目。
-
和 从 Q token 中导出。
-
从前序的 中导出。
作者选择了ReLU来提升吞吐率。即使 lightning indexer 仅有很少数量的头并且可以在 FP8 上部署,其计算效率也是非常显著的。
Step 2: 选择前 k 个索引分数最高的 , 计算注意力输出。
给定了索引分数 ,我们的细粒度 token 索引机制将会仅仅取出那些具有前 k 个索引分数的 token。随后,注意力输出 将会在当前 Q token 和稀疏化选出的 中进行。
其实是 MLA 中低秩投影计算出来的向量,用于减少 KVCache 的存储开销,提高推理效率。
下面是新旧结构的对比。上图为新的结构。下图为曾经的旧结构。


在 MLA 下实例化 DSA
为了考虑从 v3.1 继续训练,需要基于 MLA 上实例化 DSA。在 kernel 层面,每一个 KV 项都需要在多个查询之间共享,提升计算效率。因此,我们在 MLA 的 MQA 模式上部署了 DSA。这样每一个**潜在层 (latent vector)**将会在每个头之间共享(多个头共用一个潜在向量 ,也就是多个头——多个 Query,共用一个 KV)。

训练
从 v3.1-Terminus 后继续训练,上下文长度扩展到 128K。
Step 1: 稠密 warm-up 阶段
用于初始化 lightning indexer。继续保持稠密注意力机制,其余参数全部冻结,仅剩下 lightning indexer 进行训练。
为了保持 indexer 输出与原先的主要注意力分布对齐,对于第 t 个查询 token,我们首先将多个头的主要注意力分数进行相加,然后在序列维度上进行 L1-正则化,生成目标分布 . 基于 , 我们设置一个 KL-散度 loss 作为我们训练 indexer 的优化目标。
作者声称采用了 的学习率训练了 1000 步。每一步具有 128K 长度的 16 个序列,总共 2.1B 个 token。
Step 2: 稀疏训练阶段
在进行稠密训练之后,进入到了细粒度的 token 选择,并以此来优化整体模型的参数,来获得 DSA 的稀疏模式。在这一阶段,我们不在选择所有的 token,而是通过上文的方式选择通过 indexer 判断出来的,索引分数最大的 K 个 token:
需要值得注意的是我们将 indexer 的输入从计算图中分离,也就是分开 indexer 和 DSA 的其他部份,分别进行优化。
-
indexer 仍然仅仅根据 进行优化。
-
其他部分通过模型其他部分的 loss 进行优化。
稀疏训练采用学习率 ,每个 query 选择 2048 个 KV token。训练 15000 步,具有 480 个长度为 128K 的 token,总共是 943.7B token 数量。
Step 3: 后训练
后训练与先前 deepseek-v3 的后训练类似,主要有两步:
-
专家知识蒸馏。
-
混合 RL 训练。
专家知识蒸馏
-
对于每个任务我们都训练了一个专门的针对这个领域知识的模型,这些模型都是从相同的预训练 v3.2 基座模型的 ckpt 而来。
-
针对写作任务和通用问答任务,我们划分了 5 个领域:数学,竞赛类编程,通用因果逻辑,多智能体编码,多智能体搜索。
-
对于每个专家,我们都通过大规模强化学习方式进行训练。
-
并且,我们部署了不同的模型来生成针对思维链 (CoT) 的训练数据,以及直接回答 (非思维链模式) 的训练数据。
-
当专家模型完成后,他们将被用于为最后的 ckpt 生成领域专用的知识。最终 ckpt 在各个领域与专家模型的差距将通过后续的强化学习来进行弥补。
混合强化学习
-
与 v3.1 相同,仍然采用的是 GRPO 强化学习方式。
-
与前面分不同阶段强化学习不同的是,作者将**多个阶段的 RL 学习 (因果,智能体,人类对齐训练)**混合到了一起。
-
优势是可以讲多个领域的表现有效进行平衡并且设法克服在多阶段训练中造成的灾难性遗忘问题。
-
对于因果和智能体任务,我们部署了基于规则的结果奖励,长度惩罚以及语言一致性奖励。
-
对于生成式任务,我们部署了一个生成式奖励模型,将按照自己的规则进行评估。
-
reward 进行了两方面的权衡:(1) 长度 vs 准确度。(2) 一致性 vs 准确度。
评估结果
- 推理开销从原先的 (原先需要计算所有的 token,长度为 L) 变成 (Q token 长度不变,但是 KV 低秩投影 token 通过 lightning indexer 选择 K 个)。对于 lightning indexer,其计算复杂度仍然为 ,但是因为其具有的头数量比原先的 MLA 头数量少,因此常数因子的减少也显著提升了其计算效率。

竟然翻墙才能来了,好久不更新了。
最近正在进行:
GPU 的学习,参考的是《cuda by examples》
CMU15445,primer 部分刚刚完成,修复了一些 cmake 版本的问题,并且完成简单的并发跳表部分,后面会缓慢地做 lab(时间太少了)
读 sigcomm25 的论文,已经读了 stellar,接下来看看 ResCCL 和 syCCL
学习《数学物理方法》到解析函数部分。
很快也要双选了,主要做的都是数字芯片或模拟芯片方向,所以慢慢地也要复习数电和模电一下,然后到时候在问一问导师应该补上哪些基础知识吧。
读论文的时候还是深感知识的欠缺和稀少,还是要多学。最后附上一张在 autodl 上租来的 4090 画出来的 5000x5000 的 julia set~

国庆做了什么?去内蒙古玩了。但是到了景点就想睡觉。同学指定的旅游计划太折磨人了,仗着我有 C1 驾照,并且寒假开过车,就制定了雄心勃勃的旅游计划,一天开车的公里数到达了 600km。整个假期下来,アイラちゃん几乎驾驶了接近 1600km,国道,省道,乡道,高速,夜间公路,土路,草原...... 什么地方都有着驾车过的轨迹~下次旅行决定最多一天开 300km,不然太痛苦了 😭

一些照片
照片





アイラちゃん 好奇内蒙古有什么美食🐖
不知道国庆期间那里人多不多
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 热阿线狂堵 12km,提前抽身掉头走国道 😭
美食:🐑,🐑的各种做法,奶,奶的各种做法 😋
アイラちゃん 好厉害,z 一串还没有开始学车
编译了一下 NCCL。u1s1 clangd 与 nvcc 的相性太差,不能很好滴起到 LSP 的作用。
然后准备开始调研 ResCCL 和 SyCCL(都没有开源),并且更加细致地了解 all-reduce, all-gather, all-to-all 等通信源语底层的实现(ring,half-doubling 等等)。
双选开始了,方向完全就是数字 IC/模拟 IC。我实在是还没有想好该往哪个方向走,有点小纠结。等到下周再联系提供的导师看看。虽然电路/数电/模电/计算机体系结构(自学)/信号与系统/数字信号处理(自学)都学过,但是对于 CMOS/VLSI 这种完全没有任何经验。也用 verilog 写过经典 5 级流水线(填空式),和一个 tinyTPU 但是对这一方面完全没有任何经验,有点小纠结。
当然直到大四结束前都还留在原先的组里工作(学长的挽留?可能是暑假里对于仿真系统的开发挺出力的缘故),因此应该也还会对 cuda/CCL 有进一步的了解吧。未来会怎样呢我也不知道。
曾经玩过的“你和她和她的恋爱”这部 gal 里面有这样一段话:
岁月的流逝,就如同层层向上的楼梯。一旦对此产生了任何疑问,那么从那一刻起,跨上台阶的步伐将会越发沉重起来。
所以,要一步,一步,什么都不去想,就这么向上攀登。
因为是楼梯,所以也就没有分支。
“那个时候要是这么做就好了“之类的后悔是没有意义的。
而什么“人生是一连串的选择”,其实是在失败之后,人们回首往事时出于后悔而发出的,无用的感叹。向着紧接着的过去,理所当然存在着的明日。
一步,一步,向前迈进。
昨天刷 B 站看到 三秋 缒 (zhui,四声) 的《三日间的幸福》,立刻去图书馆找了一本,看了之后虽然觉得剧情有些地方转折过快,有些突兀,但是体现了作者对人生的思考,和对未来的期望。有些段落我特别喜欢。为了不剧透,进行了模糊处理~
段落一:
“大部分的失败者只会说什么如果人生能够重来,一定能创造不凡的成就,自以为在尝过人生苦楚之后,就不会再重蹈覆辙了。可是包括我,这种失败者根本就搞不清楚状况。失败的人的确十分了解何谓失败,但了解失败与了解成功完全是两码子事,就算挽回失败,也不代表就能迈向成功,因为夹在两者之间的终究只是一处灰暗的起跑点,可惜失败者们向来不了解这点。”
段落二:
“大多数的人只有在面临死亡之后,才会懂得活着多么可贵,也才能找回原有的活力,然后陷入『过去虽然百般荒唐,但如今懂得自省的我,应该还能做些什么吧?』的迷思,不过这些人都犯了要命的自以为是,因为他们不过是好不容易站回起跑点而已。简单来说,就是在输得一塌糊涂之后,才总算冷静地看待眼前的赌局。”
段落三:
“这世界绝不会突然对死到临头的人变得亲切。恐怕,这世界只对已死的人温柔。早就熟知这一切,却又耽溺于天真想法的我,心底深处果然还是存着一份全世界能瞬间变得美好的期盼啊。”
《三日间的幸福》这本作品非常慢热,但是在不现实的剧情中却总是具有非常强烈的现实感,这种真假之间的交织很容易给人一种“假作真时真亦假”的虚幻感,也很容易让染不自觉地代入到生活重复的男主身上。但是随着作品到达中期和后期,节奏陡然加快,感情线也随之剧烈跌宕起伏。爆发式的感情变化与人物的蜕变会让读者有欲罢不能的感觉——快节奏的同时能够快速地推进剧情。即使有些剧情转折过快,略显突兀,但仍然瑕不掩瑜,也能让男女主之间的感情直接叩击读者的内心,是一本非常不错的作品~
好久没在门内说话了,最近这几个月实在是太忙了。
首先还是不断地在学习新的东西,重点了解 MoE 中的 alltoall 通信,包括了 dispatch 和 combine 两部分。在这短短的几个月,关注到了 MoE 在 Infra 下的不同通信库的实现,主要都是针对 DeepEP 工作的改进和兼容:
- pplx-garden 将 p2p 通信通过 rust 实现,部署了在 AWS 上 EFA 网卡的 MoE 通信;
- NCCL 发布了 GPU-Initiated-Network,实现了任意 N 卡 + 任意网卡的灵活的 MoE 通信;
- UCCL-EP 则更进一步实现了任意 GPU+ 任意网卡的灵活的 MoE 通信;
在了解大佬们工作的过程中,也越发了解到 DeepEP 都是这些工作的来源,也更感慨于自己对于 DeepEP 的不了解和无知。在粗略地读了一小部份的代码后,更惊叹于 DeepEP 内非常细致的针对各个方面的优化:
- 对于 buffer 在不同状态下空间的计算,有下图所示:

- 在 HT 模式下和 LL 模式下采用不同数量的 QueuePair 实现网络层面的并行
- 在数据搬运的方式下采用 AsyncCopy(Ampere 引入的特性)和 TMA 操作(二维异步数据搬运,Hopper 卡引入的特性),这一点仍然需要继续学习 CUDA 才能更加了解
- 细致规定了每个 SM 的调度和使用,将计算和通信融合在同一个核心内
- NVSHMEM 借用 OpenSHMEM 的单边操作和 PGAS 内存模型提供 GPU Direct Async Kernel Initiate 支持
可以说这方面对于アイラちゃん来说更是无底洞,甚至连 C++/Cuda 基础都无法支持アイラちゃん继续探索和学习。因此 C++ 语言和 Cuda 异构基础仍然是需要常看常学的部分,尤其是异步模型和 SIMT 模型。
除此之外,最近帮助学长刚刚跑完了一份即将投稿 ToN 的实验数据,也是老生常谈的 ns3 网络仿真,这个上古的老库还是这么地折磨人。也因为这一部分差不多也告一段落(アイラちゃん的部分)才刚好可以暂时休息休息,来门内写下一长串话。
这几个月也帮助实验室做了不少 infra 方面的支持,比如各种通信库的适配,vllm 在二机四卡下的部署,点亮 BF3 网卡实现简单的逻辑卸载,DOCA 的 GPUNetIO(NVSHMEM 实现 GDAKI)的基础的简单了解。后面如果可能有机会也会探索一下 KVCache 在 DPU 上的卸载,不过看起来非常的困难,至少可以说是毫无头绪。。。。。。
接下来想要做的就是:看一些 C++/Cuda 的书,看 DeepEP 通信库,了解一些优秀的开源项目,学习别人的设计思想。一来是更了解语言提升自己程序能力的上限,二来是想要更跟进现在 Infra 的发展(实在太快了),三来是想要学会怎样应对模糊的需求,设计更合理的程序架构,减少工作量,了解一下他人的设计思想。
好久没有在门内说话了,门内的伙伴们还好吗?谢谢你们拨冗看完这一长串アイラちゃん的牢骚话,祝大家新的一年快快乐乐,事业有成~
(P.S. 魔法少女的魔女裁判真的好好玩,趁着冬促买下来了,桜羽エマ大好き!)
@tori_nankotsu96

突然发现自己之前的计划,只完成了
看完 cuda by examples
读 sigcomm25 的论文
好惭愧啊 🥺
上海今天上午下雪很大~




C++ 在写不好的情况下,重载决议出现二义性导致的不可移植性。。。
armv7(32 bits) clang
arm64(64 bits) clang
RISC-V(32 bits) clang
RISC-V(64 bits) clang
原因如下:
在 64 位系统上,
ptrdiff_t应该是long,std::size_t也应该是unsigned long, 这样就会出现
- Candidate 1:
char& BadString::operator[](unsigned long): 参数一是隐藏的BadString&完美匹配,参数二是unsigned long需要标准转换;- Candidate 2:
operator[](char*, long): 参数一需要用户定义的转换,参数二需要标准转换;这样不可能出现歧义,应该是 32 位系统上,此时
ptrdiff_t定义为int,std::size_t定义为unsigned int. 这样我们有:
- Candidate 1:
char& BadString::operator[](unsigned int): 参数一是隐藏的BadString&完美匹配,参数二是unsigned int需要标准转换;- Candidate 2:
operator[](char*, int): 参数一需要用户定义的转换,参数二完美匹配;从而出现不符合重载决议中的:“有且仅有一个候选函数,所有参数的匹配都不比其他函数差”的情况,出现二义性
好像不知道为什么被标记了,修改重发一下
例子来自: 《C++ templates The complete guide, 2nd Edition》,Page 685
アイラちゃん 不知道为什么被标记了
是被标记为 nsfw 吗 前段时间土源鉴别违规内容的 bot 发癫了 在胡乱标记
长恨此身非我有 何时忘却营营
寒假 (或者包括下学期?) 的学习计划~
leetcode 冬训
分析 deepep 库
继续学习 C++,看 C++ templates
看 Seven Concurrency Model in Seven Weeks
看新版的 cuda programming guide
准备中期答辩
紫金港第一皮革<・)))><< 
这个样子 🥺
这是上次修改

アイラちゃん 鉴定为 bot 发癫了()
其实或许也可以等站务审核 人工取消对帖子的隐藏
长恨此身非我有 何时忘却营营
紫金港第一皮革<・)))><< 没事,直接改了更好一点,就不用等了的说 😋
アイラちゃん 继续学习 C++,看 C++ templates
https://federico-busato.github.io/Modern-CPP-Programming/
The Modern C++ Programming course was created in 2018 and adopted by the University of Verona, Italy. It is taught as part of the Bachelor’s and Master’s degree programmes in Computer Science, for three and two years respectively. The course has also been used to train interns at NVIDIA.
アイラちゃん 学到了
有且仅有一个候选函数,所有参数的匹配都不比其他函数差
虽然这个例子看起来像新品种的 cpp 字符串笑话(x
准备回家了,基本上事情已经忙的差不多了,接下来一个月学习的将会非常杂,所以アイラちゃん的日记楼里将会有各种方向的奇怪知识
今天记录的是 Rust 里面比较复杂的 所有权 (ownership), 范围 (scope), 引用 (Reference) 和 借用 (borrowing),我们通称为“移动语义 (moving semantics)”, 和 C++ 还是有些不太相同的。
Ownership & Scope
主要有三条规则:
- 每个值将必须有且仅有一个 owner。
- 超出一定范围的时候,owner 将放弃这个值。
- 大括号可以指定变量的范围。如果没有,则变量的范围持续到最后一次使用这个变量的地方。
对于规则一,我们有:
let s1 = String::from("Hello");
let s2 = s1;
// println!({s1}) 非法,因为每个值只能在同一个 scope 内有一个 owner;
对于规则二,我们有 隐形 drop 机制。
{
let s: String = String::from("Hello")
// 这里我们将有一个隐形的 drop
}
// println!("{s}") 非法,s 已经被 drop
为什么这么设计?
因为在 Rust 中,每个 String 的值是分配在堆 (malloc) 上的,我们拿到的只是一个类似于结构体变量的东西,其中有 (ptr 指针指向那一段堆上内存,len 代表长度,capacity 代表容量) 的成员,在超出 scope 将调用隐形的 drop 释放内存的规则下,如果一个值(一段内存)被两个变量的 drop 同时释放,就会出现 double free 的情况,造成出错。
如果我们想要 deepcopy,我们需要用 .clone() 来进行。
函数和所有权
caller(调用者) 会把非栈上变量的值的所有权移交给 callee(被调用者)。也就是如下所示:
fn foo(s: String) {
// do something;
}
fn main() {
let s1 = String::from("Hello");
foo(s1);
// println!("{s1}"); 非法,因为 s1 已经无效了。
}
同样地,callee(被调用者) 也可以通过返回非栈上变量的方式让值传递到 scope 之外。
fn foo() -> String {
let s = String::from("Hello");
s // 没有;代表这是一个 expression , 将会有返回值。
// 有; 代表这是一个 statement,执行一个动作。
} // 离开 s 的变量范围,s 失效
fn main() {
let s1 = foo(); // s1 = "hello"
}
这样我们发现:
caller 将变量的值传递给 callee,变量失效
-> callee 执行操作
-> 将变量传递回来给 caller
如果我们不想让变量在 caller 中失效怎么办?我们就需要采用 Reference,borrowing,mutable reference 了。
(mutable) Reference, borrowing
如果我们不想让变量在 caller 中失效,我们可以传递一个 Reference。如下:
fn main() {
let s1 = String::from("hello"); // "hello" 的 owner: s1
let len = calculate_length(&s1); // 传递 s1 的引用,同时保持 s1 有效
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len() // expression 返回 s 的长度
}
但是我们注意到,此时 callee 中的 s 是不可变的。如果我们需要修改,我们就需要传递可变引用 (mutable references)。
fn change_and_return_length(s: &mut String) -> usize {
s.push_str(" world!");
s.len()
}
fn main() {
let mut s1 = String::from("Hello");
let len = change_and_return_length(&mut s1);
println!("s1: {s1} has len {len}");
}
还记得上文的规则三吗?规则三说:
大括号可以指定变量的范围。如果没有,则变量的范围持续到最后一次使用这个变量的地方。
对于可变的引用,我们要求针对同一个变量,只能有一个可变引用。但是多个引用是可以的。这样,我们可以在编译期防止数据竞争。
什么时候出现 Data Race?
- 一个值在同一时间被多个指针指向;
- 至少有一个指针在修改这个值;
- 没有同步机制来保证对这个值的访问;
我们来看这个例子:
fn main() {
let mut x = Vec::new(); // x start;
let y = &mut x; // y 可变引用 x; y start;
y.push(42); // 最后一次使用 y,y end,超出 y 的 scope
let z = &mut x; // z 可变引用 x; z start;
z.push(13); // 最后一次使用 z, z end, 超出 z 的 scope
assert_eq!(x, [42, 13]);
}
诶?为什么这里有使用了多次对 x 的引用呢?(回忆我们的规则 3)
悬垂引用
最后我们来看一下这个例子:
fn danger() -> String& {
let s = String::from("Hello");
&s
}
fn safe() -> String {
let s = String::from("Hello");
s
}
我们返回一个对 s 的引用,但是 s 在超出 scope 后就会消亡,这就会造成悬垂引用。正确的做法是上面的 safe。因为这时候 s 的值将会转移所有权到范围外,s 消亡。
水文:如何在 Mac 上使用 views::enumerate?
回到家中几乎什么都不想干,荒废了 🥺
起因是一道简单题
在尝试学习 ranges 和 views 的 アイラちゃん 显然不会放弃“简单题复杂做”的雷霆想法,因此就尝试写出了面条代码:
class Solution {
public:
vector<int> constructTransformedArray(vector<int> &nums) {
int n = nums.size();
return nums | views::enumerate | views::transform([&](auto &&tup) { const auto &[i, num] = tup;
return nums[((i + num) % n + n) % n]; }) | std::ranges::to<std::vector>();
}
};
可以运行 [1] 来查看这个执行的结果。
pipeline 运算符 |
我们首先来看 pipeline 运算符的定义:在 C++20 的 ranges 库中有这样的定义:
template<typename _Self, typename _Range>
requires __is_range_adaptor_closure<_Self>
&& __adaptor_invocable<_Self, _Range>
constexpr auto
operator|(_Range&& __r, _Self&& __self)
{ return std::forward<_Self>(__self)(std::forward<_Range>(__r)); }
- requires 子句要求_Self 类是一个 range_adaptor_closure 类型,这个类型是一个辅助模板类型,用于定义
RangeAdaptorClosureObject。RangeAdaptorClosureObject其实就构成了 pipeline 运算符|的语义,如果我们有一个对象 C 是一个RangeAdaptorClosureObject,一个 R 是一个Range,那么我们就有以下的等价语义:[2]C(R) <=> R | C- 更复杂一点,我们可以将多个闭包串起来:
d(c(R)) <=> R | c | d
- operator
|将会利用经典的完美转发std::forward,因此我们实际上将会调用的是|右侧的 operator()的重载。 views::enumerate将会把 range 转换成一个 tuple (index, elements),在迭代过程中我们可以获取当前迭代器的下标。
这样我们的例子实际上等效于 [3]:
int n = nums.size();
std::vector<int> ans = std::ranges::to<std::vector>(
std::views::transform(
[&](auto &&tup) {
const auto &[i, num] = tup;
return nums[((i + num) % n + n) % n];
})(
std::views::enumerate(nums)));
实际上使用的时候不要想的这么复杂。想象 | 就是一根水管。在调用 std::next(),也就是迭代的时候,| 左侧将会有一个迭代器对应的元素,经过 | 右侧的变换,得到新的元素。就像 next 是水阀,每调用一次就会将一个元素送入水管,经过加工后输出新的成品。
在 rust 里这样的写法更加常见。我们可以:
fn construct_transformed_array(nums: Vec<i32>) -> Vec<i32> {
let n = nums.len();
let ans:Vec<i32> = nums
.iter()
.enumerate()
.map(|(i, &num)| nums[(((i as i32 + num as i32) % (n as i32) + (n as i32)) % (n as i32)) as usize])
.collect();
ans
}
编译不通过!CMake 尝试混搭 clang+libstdc++
但是问题来了,我们在 Mac 上编译不通过!!!经过查询以后,我们很遗憾地发现 [4]:

这又是折腾的一个好机会,我们既不想放弃 LLVM 提供的编译器和 LLDB,也想使用 views::enumerate,但是 LLVM 对应的 libc++ 又没有实现,该怎么办呢?我们就需要在 CMakeLists.txt 里面进行魔改,来在前端和中间端采用 LLVM,后端链接上 libstdc++。
我们还是用 brew 来折腾我们的 mac:
brew install llvm
brew install gcc
这样我们就有了一个 clang 21 和一个 gcc 15.2。
然后我们开始编写 CMakeLists 来魔改我们的后端。首先还是经典起手:
cmake_minimum_required(VERSION 3.26)
project(main)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
接着我们寻找 gcc 的相关路径和头文件,设置我们的 clang 编译器,查找对应的 GCC 及其版本。
# 可以利用 which clang which gcc 来找。
# 一般在 linux brew 上为:/home/linuxbrew/.linuxbrew/opt/xxx
set(LLVM_PREFIX "/opt/homebrew/opt/llvm")
file(GLOB GCC_PREFIX "/opt/homebrew/opt/gcc")
set(CMAKE_C_COMPILER "${LLVM_PREFIX}/bin/clang")
set(CMAKE_CXX_COMPILER "${LLVM_PREFIX}/bin/clang++")
# 查找 GCC 版本 (我们这里是 15)
file(GLOB GCC_INCLUDE_DIRS "${GCC_PREFIX}/include/c++/*")
list(GET GCC_INCLUDE_DIRS 0 GCC_CPP_PATH)
get_filename_component(GCC_VER "${GCC_CPP_PATH}" NAME)
message(STATUS "Found GCC Path: ${GCC_PREFIX}")
message(STATUS "Using libstdc++ version: ${GCC_VER}")
禁用相关的 clang 的默认 libc++ 的库,并且 LLVM 生成的调试信息为 gdwarf-5,我们的 Mac 自带的链接器 ld 仅支持 gdwarf-4 的调试信息。因此我们也采用 brew install lld,加上:
add_compile_options("-nostdinc++")
add_link_options("-fuse-ld=lld")
我们需要依照顺序注入 GCC 的头文件,提供给前端的 Clang:
include_directories(SYSTEM "${GCC_CPP_PATH}")
# 这里根据 ls /opt/homebrew/opt/gcc/include/c++/15 中的第一个文件夹就是。这里是重要的根据对应的系统架构的相关配置,供 C++ 的内置函数进行系统调用。
include_directories(SYSTEM "${GCC_CPP_PATH}/aarch64-apple-darwin24")
# 兼容性文件,是一些古老的玩意儿~
include_directories(SYSTEM "${GCC_CPP_PATH}/backward")
最后补充全 clang 的相关配置,防止 stddef.h 之类的文件是丢失的导致编译失败,链接时提示链接器去 GCC 的 lib 中寻找 libstdc++。
execute_process(COMMAND ${CMAKE_CXX_COMPILER} -print-resource-dir
OUTPUT_VARIABLE CLANG_RESOURCE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
include_directories(SYSTEM "${CLANG_RESOURCE_DIR}/include")
add_link_options("-L${GCC_PREFIX}/lib/gcc/${GCC_VER}")
add_link_options("-stdlib=libstdc++")
# 显示设置 rpath。
add_link_options("-Wl,-rpath,${GCC_PREFIX}/lib/gcc/${GCC_VER}")
最后加上构建目标就可以了。
include_directories(templates) # 其他文件夹
add_executable(main main.cpp) # 目标文件
这样我们就完成了利用 clang + libstdc++ 的魔幻搭配。
什么时候使用 range?
接下来其实有一个问题:我们什么时候使用 ranges?什么时候使用 for loop?ranges 的这类 pipe 运算符,以及 Rust 中的 map 都看起来和 for loop 的表现很像,但其实也有很大的区别。
在 C++ 中,ranges 通常比 for loop 慢 [5],而 Rust 的性能相近 [6]。原因我还不太清楚。
而在 Rust 中,我们其实有了一点点提示,关于什么时候使用 ranges[7]:
map() is conceptually similar to a for loop. However, as map() is
lazy, it is best used when you’re already working with other iterators.
If you’re doing some sort of looping for a side effect, it’s considered
more idiomatic to use for than map().
在执行迭代(消费)操作的时候采用 iters,而在拥有外部修改效果(打印,完全遍历 IO)的时候,我们则需要进行 for loop。
アイラちゃん 回到家中几乎什么都不想干
我在校期间也是这样🥺
长恨此身非我有 何时忘却营营
アイラちゃん 在执行迭代(消费)操作的时候采用 iters,而在拥有外部修改效果(打印,完全遍历 IO)的时候,我们则需要进行 for loop。
唉函数式编程
在这里提前祝塬友们新春快乐!
不过アイラちゃん一直都不喜欢这种太过于热闹的节日,社交特别耗费精力而且热闹过头的节日反而让アイラちゃん感觉不适应,还是更喜欢独自一人 + 赛博网友的双休日一点。好不容易刚刚才从社交地狱中跑出来,被父母硬扯去打麻将(但实际上一点都不会)然后输个精光,在一桌的人嘴上都说的“一家人”但实际上不动声色地把你的钱一点点“骗”走的感觉真是不舒服呢~(笑)
不管怎么样,就让最后的两个小时带去身上不快乐的记忆,迎接农历新的一年吧!
X: @ArameDraw

アイラちゃん 社交特别耗费精力
同意,尤其是向上社交 😇
Hard time comes again and never leave.
从来没觉得学习C++快乐过www——分享一个splice函数的坑
不用_LIBCPP_DEBUG或者_GLIBCXX_DEBUG时,Asan会查不出误用stl的内存问题。并且 cmake --DCMAKE_BUILD_TYPE=debug 不会定义std debug的宏。你需要自己add_compile_options进去。
我们分享三个例子。
仅开启Asan-不能查到错误
仅定义_LIBCPP_DEBUG-出现segmentation fault
开启Asan+定义_LIBCPP_DEBUG-Asan报错并且出现segmentation fault
一开始在实现的时候,我发现 list.splice(pos, other, it) 中的other好像是哪一个list都无所谓。后来我们(我和gemini)发现,这个other是传入对应的list,在splice转移之后减少这个other的size大小而用的。误用会导致size大小错误。
不学C++了,学rust去了 (生气) 🐖







