平衡树

平衡树

  • 定义

    全称为二叉平衡搜索树。

  • 二叉搜索树

    形式化的,这个概念是指一棵节点带权的二叉树,令节点 \(i\) 的权值为 \(w_i\),左子树点集为 \(L\),右子树点集为 \(R\),其满足 \(\forall u \in L,w_u \le w_i\)\(\forall v \in R,w_v > w_i\)

  • 性质 / 作用

    • 具备单调性,可实现 \(\log\) 级别复杂度的查找。

    • 中序遍历单调不减,于是可以与一个序列形成映射(将下标作为权值即可)。

  • 平衡的意义

    众所周知,一个序列对应的二叉搜索树不一定惟一。我们希望取得高度更小的那一棵,以保证复杂度。

    若一棵二叉树的任意节点 \(i\),其左右子树高度差 \(\Delta h \le 1\),则我们称其为一棵二叉平衡树

  • 常用的平衡树

    treap、fhq-treap、splay。

FHQ-Treap

  • 定义

    又称无旋 Treap。即仅靠分裂与合并完成维护。

  • \(\operatorname{split}(k,a,b,val)\)

    顾名思义,即将根节点为 \(k\) 的 tree 分裂成根节点为 \(a,b\) 的两棵 tree,点集为 \(A,B\),其中 \(\forall u \in A, w_u \le val\)\(\forall v \in B, w_v > val\)

    大体的思路就是 \(k\) 为当前节点,若 \(w_k \le val\),说明找到了 \(a\),并且其左子树也都在 \(A\) 中,然后去右子树中寻找 \(b\)\(A\) 所缺的右子树。找到了 \(b\) 则反之处理即可。

  • \(\operatorname{merge}(k,a,b)\)

    顾名思义,即将根节点为 \(a,b\) 的 tree 合并为根节点为 \(k\) 的 tree,点集为 \(A,B\),其中 \(\forall u \in A,v \in B,w_u \le w_v\)

    根据定义,肯定是 \(A\)\(B\) 右了,但是高度无法确定。

    于是,我们给每个节点随机一个 \(rank\),谁 \(rank\) 小谁就在高处,这样退化成一条链的几率很小,可以保证均摊复杂度 \(\log\) 级别。

    确定了高度,譬如 \(A\) 在上,那么 \(B\) 就需要和 \(A\) 的右子树继续竞争,递归下去即可。

这两个函数是 FHQ-Treap 的精髓之所在。

其他的因题而异,请自行阅读代码进行理解。本代码只给出函数部分。

实现
void upd(int k){
tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
int add_node(int val){
tree[++tot].siz=1;
tree[tot].val=val;
tree[tot].rnk=rand();
tree[tot].lt=tree[tot].rt=0;
return tot;
}
void split(int k,int &a,int &b,int val){
if(!k){
a=b=0;
return;
}
if(tree[k].val<=val){
a=k;
split(tree[k].rt,tree[k].rt,b,val);
}
else{
b=k;
split(tree[k].lt,a,tree[k].lt,val);
}
upd(k);
}
void merge(int &k,int a,int b){
if(!a||!b){
k=a+b;
return;
}
if(tree[a].rnk<tree[b].rnk){
k=a;
merge(tree[k].rt,tree[k].rt,b);
}
else{
k=b;
merge(tree[k].lt,a,tree[k].lt);
}
upd(k);
}
void ins(int &k,int val){
int a=0,b=0,cur=add_node(val);
split(k,a,b,val);
merge(a,a,cur);
merge(k,a,b);
}
void del(int &k,int val){
int a=0,b=0,c=0;
split(k,a,b,val);
split(a,a,c,val-1);
merge(c,tree[c].lt,tree[c].rt);
merge(a,a,c);
merge(k,a,b);
}
int fnd_rnk(int &k,int val){
int a=0,b=0;
split(k,a,b,val-1);
int res=tree[a].siz+1;
merge(k,a,b);
return res;
}
int fnd_num(int k,int x){
while(tree[tree[k].lt].siz+1!=x){
if(tree[tree[k].lt].siz>=x)
k=tree[k].lt;
else{
x-=tree[tree[k].lt].siz+1;
k=tree[k].rt;
}
}
return tree[k].val;
}
int pre(int &k,int val){
int a=0,b=0;
split(k,a,b,val-1);
int res=fnd_num(a,tree[a].siz);
merge(k,a,b);
return res;
}
int suf(int &k,int val){
int a=0,b=0;
split(k,a,b,val);
int res=fnd_num(b,1);
merge(k,a,b);
return res;
}

应用

P1486(整体修改、第 \(K\) 大)

看到如上的关键词,我们考虑用一棵平衡树维护工资档案。

具体的:

  • I k:新建并插入一个节点。

  • A k:维护一个 \(tag\) 表示整体修改的值,令 \(tag+k \to tag\)

  • S k:令 \(tag-k \to tag\),并删除权值 \(< min-tag\)注意不是 \(<min\))的所有节点。

  • F k:查询第 \(k\) 大即可。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,ans,root,tot,mini,tag,all;
struct fhq{
int val,rnk,siz,lt,rt;
}tree[N];
int add_node(int val){
tree[++tot].val=val,all++;
tree[tot].siz=1;
tree[tot].rnk=rand();
tree[tot].lt=tree[tot].rt=0;
return tot;
}
void upd(int k){
tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void split(int k,int &a,int &b,int val){
if(!k){
a=b=0;
return;
}
if(tree[k].val<=val){
a=k;
split(tree[k].rt,tree[k].rt,b,val);
}
else {
b=k;
split(tree[k].lt,a,tree[k].lt,val);
}
upd(k);
}
void merge(int &k,int a,int b){
if(!a||!b){
k=a+b;
return;
}
if(tree[a].rnk<tree[b].rnk){
k=a;
merge(tree[k].rt,tree[k].rt,b);
}
else {
k=b;
merge(tree[k].lt,a,tree[k].lt);
}
upd(k);
}
void ins(int &k,int val){
int a=0,b=0,cur=add_node(val);
split(k,a,b,val);
merge(a,a,cur);
merge(k,a,b);
}
void del(int &k){
int a=0,b=0;
split(k,a,b,mini-tag-1);
ans+=tree[a].siz;
all-=tree[a].siz;
k=b;
}
int fnd_num(int k,int x){
while(tree[tree[k].rt].siz+1!=x){
if(tree[tree[k].rt].siz>=x)
k=tree[k].rt;
else{
x-=tree[tree[k].rt].siz+1;
k=tree[k].lt;
}
}
return tree[k].val;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
srand(time(0));
cin>>n>>mini;
add_node(-1e18),add_node(1e18);
all=0;
while(n--){
char x; int k;
cin>>x>>k;
if(x=='I'&&k>=mini)
ins(root,k-tag);
else if(x=='A')
tag+=k;
else if(x=='S')
tag-=k,del(root);
else if(x=='F'){
if(all<k)
cout<<"-1\n";
else
cout<<fnd_num(root,k)+tag<<'\n';
}
}
cout<<ans;
return 0;
}

总结:

  • \(K\) 大考虑平衡树。

  • 整体修改考虑给整棵树打标记。

P2234(前驱、后继)

我们仍然考虑使用一棵平衡树维护营业额。

对于每天的营业额,我们顺次插入平衡树,然后在其前驱和后继这两个之间找差最小的即可。

需要注意的是:

  • 如果前面出现过相同的营业额,绝对差直接为 \(0\)

  • 应当设置哨兵节点以防找不到前驱 / 后继。

实现

总结:

  • 差最小考虑平衡树维护前驱 / 后继。

P3224(与 DSU 的结合)

还是查询第 \(K\) 小,考虑平衡树维护。

很容易想到对于每个联通块建一棵平衡树,然后用并查集维护联通性(合并)的时候顺便把树也合并了。

但是,我们的 merge 函数只能接受满足 \(\forall u \in A,v \in B,u<v\) 的点集为 \(A,B\) 的两棵树进行合并,可联通块并不具有此种性质。

于是启发式合并,将较小的树的点一个一个地扔进较大的树中即可,时间复杂度是 \(\log\) 级别的(一个点至多会被合并 \(\log n\) 次,因为每次合并都会使大小翻倍)。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;
const int N=2e5+5;
int n,m,q;
int root[N],fa[N],tot;
struct fhq{
int val,rnk,siz,lt,rt,id;
}tree[N<<2];
int fnd(int x){
return (fa[x]==x?x:fa[x]=fnd(fa[x]));
}
int add_node(int val,int id){
tree[++tot].val=val;
tree[tot].siz=1;
tree[tot].rnk=rand();
tree[tot].lt=tree[tot].rt=0;
tree[tot].id=id;
return tot;
}
void upd(int k){
tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void split(int k,int &a,int &b,int val){
if(!k){
a=b=0;
return;
}
if(tree[k].val<=val){
a=k;
split(tree[k].rt,tree[k].rt,b,val);
}
else {
b=k;
split(tree[k].lt,a,tree[k].lt,val);
}
upd(k);
}
void merge(int &k,int a,int b){
if(!a||!b){
k=a+b;
return;
}
if(tree[a].rnk<tree[b].rnk){
k=a;
merge(tree[k].rt,tree[k].rt,b);
}
else {
k=b;
merge(tree[k].lt,a,tree[k].lt);
}
upd(k);
}
void ins(int &k,int val,int id){
int a=0,b=0,cur=add_node(val,id);
split(k,a,b,val);
merge(a,a,cur);
merge(k,a,b);
}
void cls(int x){
tree[x].val=tree[x].rnk=tree[x].siz=tree[x].lt=tree[x].rt=tree[x].id=0;
}
void mrg(int x,int y){
x=fnd(x),y=fnd(y);
if(x==y)
return;
if(tree[root[y]].siz>tree[root[x]].siz)
swap(x,y);
fa[y]=x;
while(1){
ins(root[x],tree[root[y]].val,tree[root[y]].id);
cls(root[y]);
merge(root[y],tree[root[y]].lt,tree[root[y]].rt);
if(!root[y]){
root[y]=root[x];
break;
}
}
}
int fnd_num(int k,int x){
while(tree[tree[k].lt].siz+1!=x){
if(tree[tree[k].lt].siz>=x)
k=tree[k].lt;
else{
x-=tree[tree[k].lt].siz+1;
k=tree[k].rt;
}
}
return tree[k].id;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
srand(time(0));
cin>>n>>m;
for(int i=1,x;i<=n;i++)
cin>>x,root[i]=i,fa[i]=i,add_node(x,i);
for(int i=1,u,v;i<=m;i++)
cin>>u>>v,mrg(u,v);
cin>>q;
while(q--){
char opt; int x,y;
cin>>opt>>x>>y;
if(opt=='Q')
cout<<(tree[root[fnd(x)]].siz<y?-1:fnd_num(root[fnd(x)],y))<<'\n';
else
mrg(x,y);
}
return 0;
}

总结:

  • merge 不行,启发式合并来凑。

P3391(区间翻转问题、按 size 分裂)

首先,我们考虑将序列映射到这棵平衡树的中序遍历上。

这样,按照 \([1,r] \to [l,r]\) 的方式分裂两次即可得到 \([l,r]\) 区间。

然后翻转这东西,它就像异或一样,做两次相当于没做。

于是我们考虑对每个节点打标记,于是每次反转操作只需要分出区间再更新标记(别忘了把当前节点的左右子树反过来)即可。

哦对了,最后输出的时候可以一边中序遍历、一边继续下传标记(中间不一定下传完了)。

关于实现,分裂的时候需要写一个按 size 分裂的 fhq-treap,具体看代码8。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m,root,tot;
struct fhq{
int val,rnk,siz,lt,rt,tag;
}tree[N];
int add_node(int val){
tree[++tot].val=val;
tree[tot].siz=1;
tree[tot].rnk=rand();
tree[tot].lt=tree[tot].rt=0;
return tot;
}
void pushup(int k){
tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void pushdown(int k){
if(!tree[k].tag)
return;
swap(tree[k].lt,tree[k].rt);
tree[tree[k].lt].tag^=1;
tree[tree[k].rt].tag^=1;
tree[k].tag=0;
}
void split(int k,int &a,int &b,int x){
if(!k){
a=b=0;
return;
}
pushdown(k);
if(tree[tree[k].lt].siz+1>x){
b=k;
split(tree[k].lt,a,tree[k].lt,x);
}
else {
a=k;
split(tree[k].rt,tree[k].rt,b,x-tree[tree[k].lt].siz-1);
}
pushup(k);
}
void merge(int &k,int a,int b){
if(!a||!b){
k=a+b;
return;
}
pushdown(a);
pushdown(b);
if(tree[a].rnk<tree[b].rnk){
k=a;
merge(tree[k].rt,tree[k].rt,b);
}
else {
k=b;
merge(tree[k].lt,a,tree[k].lt);
}
pushup(k);
}
void ins(int &k,int val){
int a=0,b=0,cur=add_node(val);
split(k,a,b,val);
merge(a,a,cur);
merge(k,a,b);
}
void qry(int &k,int l,int r){
int a=0,b=0,c=0;
split(k,a,b,r);
split(a,a,c,l-1);
tree[c].tag^=1;
merge(a,a,c);
merge(k,a,b);
}
void dfs(int k){
if(!k)
return;
pushdown(k);
dfs(tree[k].lt);
cout<<k<<' ';
dfs(tree[k].rt);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
srand(time(0));
cin>>n>>m;
for(int i=1;i<=n;i++)
ins(root,i);
for(int i=1,l,r;i<=m;i++){
cin>>l>>r;
qry(root,l,r);
}
dfs(root);
return 0;
}

总结:

  • 序列问题考虑映射到平衡树的中序遍历上,以及使用按 size 分裂的。

  • 数据结构注重打标记(对每个节点打标记最常见),然后注意下传。

P5217(按 size 分裂查排名、状压思想)

全家桶了属于是。

插入、删除、翻转、查排名对应的字母都不讲。

P 操作就是查字母对应的排名,具体而言就是从 \(x\) 开始,若它是右儿子,那么不停地往上跳,然后累加左子树的节点数,很容易理解吧。特别注意翻转标记必须先下传完再处理这个操作,不然会导致「当前文本」不是真正的当前文本。

Q 操作这种种类数的询问显然考虑状压维护,每次 pushup 时或一下左右子树的二进制值再传给父亲即可。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
//#define int long long
using namespace std;
const int N=2e5+5;
int n,m,tot,root;
string str;
struct fhq{
int tag,val,rnk,lt,rt,siz,word,fa;
}tree[N];
void pushup(int k){
if(tree[k].lt)
tree[tree[k].lt].fa=k;
if(tree[k].rt)
tree[tree[k].rt].fa=k;
tree[k].word=tree[tree[k].lt].word|tree[tree[k].rt].word|1<<tree[k].val;
tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void pushdown(int k){
if(!tree[k].tag)
return;
swap(tree[k].lt,tree[k].rt);
tree[tree[k].lt].tag^=1;
tree[tree[k].rt].tag^=1;
tree[k].tag=0;
}
int add_node(int val){
tree[++tot].siz=1;
tree[tot].lt=tree[tot].rt=0;
tree[tot].word=0;
tree[tot].val=val;
tree[tot].rnk=rand();
tree[tot].tag=0;
pushup(tot);
return tot;
}
void split(int k,int &a,int &b,int x){
if(!k){
a=b=0;
return;
}
pushdown(k);
if(tree[tree[k].lt].siz+1>x){
b=k;
split(tree[k].lt,a,tree[k].lt,x);
tree[a].fa=0;
}
else{
a=k;
split(tree[k].rt,tree[k].rt,b,x-tree[tree[k].lt].siz-1);
tree[b].fa=0;
}
pushup(k);
}
void merge(int &k,int a,int b){
if(!a||!b){
k=a+b;
return;
}
pushdown(a),pushdown(b);
if(tree[a].rnk<tree[b].rnk){
k=a;
merge(tree[k].rt,tree[k].rt,b);
}
else{
k=b;
merge(tree[k].lt,a,tree[k].lt);
}
pushup(k);
}
void dddd(int k){
if(tree[k].fa)
dddd(tree[k].fa);
pushdown(k);
}
void dfs(int k){
if(!k)
return;
pushdown(k);
dfs(tree[k].lt);
cout<<(char)(tree[k].val+'a');
dfs(tree[k].rt);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
srand(time(0));
cin>>n>>m>>str;
str='#'+str;
for(int i=1;i<=n;i++)
merge(root,root,add_node(str[i]-'a'));
while(m--){
char op,ch;
int x,y,a=0,b=0,c=0;
cin>>op>>x;
if(op=='I'){
cin>>ch;
split(root,a,b,x);
merge(a,a,add_node(ch-'a'));
merge(root,a,b);
}
else if(op=='D'){
split(root,a,b,x);
split(a,a,c,x-1);
tree[c].val=-1;
merge(root,a,b);
}
else if(op=='R'){
cin>>y;
split(root,a,b,y);
split(a,a,c,x-1);
tree[c].tag^=1;
merge(a,a,c);
merge(root,a,b);
}
else if(op=='P'){
if(tree[x].val==-1){
cout<<"0\n";
continue;
}
dddd(x);
int res=tree[tree[x].lt].siz+1;
for(int i=x;tree[i].fa;i=tree[i].fa)
if(tree[tree[i].fa].rt==i)
res+=tree[tree[tree[i].fa].lt].siz+1;
cout<<res<<'\n';
}
else if(op=='T'){
split(root,a,b,x);
split(a,a,c,x-1);
cout<<(char)(tree[c].val+'a')<<'\n';
merge(a,a,c);
merge(root,a,b);
}
else if(op=='Q'){
cin>>y;
split(root,a,b,y);
split(a,a,c,x-1);
cout<<__builtin_popcount(tree[c].word)<<'\n';
merge(a,a,c);
merge(root,a,b);
}
//dfs(root);
//cout<<'\n';
}
return 0;
}

总结:

  • 积累了按 size 分裂的 fhq_treap 如何查排名。

  • 种类数的维护考虑状压。

P3765(摩尔投票、随机化)

积累一个摩尔投票(求区间内绝对众数)的 trick:

在一个区间内,首先令第一个元素为答案;维护一个 \(cnt\),之后每遇到一个与答案相同的,\(cnt+1 \to cnt\),否则 \(cnt-1 \to cnt\)。若 \(cnt=0\),则令当前这个为答案,继续重复上述步骤,最后的答案就有可能为绝对众数。正确性显然。

很容易发现摩尔投票具有区间可加性,我们就可以使用线段树维护绝对众数。具体而言,若两个区间众数相同,则合起来众数不变、\(cnt\) 相加;否则众数为较大的那个、\(cnt\) 为绝对差。这样在保证存在绝对众数的情况下,可以做到 \(O(1)\) 回答询问。

问题是现在无法保证存在绝对众数,于是我们需要进行验证,也就是验证求出的众数是否真的有 \(r-l+1\) 个人投他。考虑使用平衡树维护之,将每个人插入他投的那个人的平衡树中,于是问题等价于询问排名 \(l \sim r\) 中是否有 \(r-l+1\) 个人,所以我们需要 \(n\) 棵按 val 分裂的平衡树(方便查找排名)。

然后这个题就做完了,但是巨大难写,怎么办?

考虑乱搞。具体的,我们完全可以不需要线段树维护区间绝对众数,我们直接随机一个,再用平衡树检查。只随机一次的错误概率为 \(\frac{1}{2}\),这样重复 \(15 \sim 20\) 次,错误概率最低仅为 \(\frac{1}{2^{20}}\),几乎可以忽略不计。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
//#define int long long
using namespace std;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9')
x=x*10+ch-'0',ch=getchar();
return x*f;
}
void write(int x)
{
if(x<0)
putchar('-'),x=-x;
if(x>9)
write(x/10);
putchar(x%10+'0');
return;
}
const int N=5e6+5;
int n,m,root,tot;
int a[N];
bool vis[N];
struct FHQ{
int val,rnk,siz,lt,rt;
}tree[N];
struct fhq{
int root=0;
int add_node(int val){
tree[++tot].val=val;
tree[tot].siz=1;
tree[tot].rnk=rand();
tree[tot].lt=tree[tot].rt=0;
return tot;
}
void upd(int k){
tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void split(int k,int &a,int &b,int val){
if(!k){
a=b=0;
return;
}
if(tree[k].val<=val){
a=k;
split(tree[k].rt,tree[k].rt,b,val);
}
else {
b=k;
split(tree[k].lt,a,tree[k].lt,val);
}
upd(k);
}
void merge(int &k,int a,int b){
if(!a||!b){
k=a+b;
return;
}
if(tree[a].rnk<tree[b].rnk){
k=a;
merge(tree[k].rt,tree[k].rt,b);
}
else {
k=b;
merge(tree[k].lt,a,tree[k].lt);
}
upd(k);
}
void ins(int val){
int a=0,b=0,cur=add_node(val);
split(root,a,b,val);
merge(a,a,cur);
merge(root,a,b);
}
void del(int val){
int a=0,b=0,c=0;
split(root,a,b,val);
split(a,a,c,val-1);
merge(c,tree[c].lt,tree[c].rt);
merge(a,a,c);
merge(root,a,b);
}
int fnd_rnk(int val){
int a=0,b=0;
split(root,a,b,val);
int res=tree[a].siz;
merge(root,a,b);
return res;
}
}tr[N];
int qry(int l,int r){
int res=-1,stk[30]={0},top=0;
for(int i=1;i<=15;i++){
int seed=a[rand()%(r-l+1)+l];
if(vis[seed])
continue;
vis[seed]=1,stk[++top]=seed;
if(tr[seed].fnd_rnk(r)-tr[seed].fnd_rnk(l-1)>(r-l+1)/2){
res=seed;
break;
}
}
while(top)
vis[stk[top]]=0,top--;
return res;
}
signed main(){
srand(time(0));
n=read(),m=read();
for(int i=1;i<=n;i++)
a[i]=read(),tr[a[i]].ins(i);
for(int i=1,l,r,s,k;i<=m;i++){
l=read(),r=read(),s=read(),k=read();
int winner=qry(l,r);
if(winner==-1)
winner=s;
for(int j=1,x;j<=k;j++)
x=read(),tr[a[x]].del(x),a[x]=winner,tr[winner].ins(x);
write(winner),putchar('\n');
}
write(qry(1,n));
return 0;
}

总结:

  • 区间绝对众数:摩尔投票 / 随机化。

  • 注意平衡树的使用是按 size(一般用于序列问题) 还是 val(一般用于查排名、前驱、后继等) 分裂。

posted @   _XOFqwq  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
历史上的今天:
2024-03-09 Living-Dream 系列笔记 第49期
2024-03-09 Living-Dream 系列笔记 第1期
2024-03-09 Living-Dream 系列笔记 第2期
2024-03-09 Living-Dream 系列笔记 第3期
2024-03-09 CF1846D Rudolph and Christmas Tree 题解
2024-03-09 Living-Dream 系列笔记 第4期
2024-03-09 Living-Dream 系列笔记 第5期
点击右上角即可分享
微信分享提示