2024“钉耙编程”中国大学生算法设计超级联赛(5)
写在前面
补题地址:https://acm.hdu.edu.cn/listproblem.php?vol=65,题号 7481~7493。
以下按个人难度向排序。
比较顺利的一场,今天双人双题环节没有卡太久,赢!
置顶广告:中南大学 ACM 集训队绝赞招新中!
有信息奥赛基础,获得 NOIP 省一等奖并达到 Codeforces rating 1900+ 或同等水平及以上者,可以直接私聊我与校队队长联系,免选拔直接进校集训队参加区域赛!
没有达到该水平但有志于 XPCX 赛事请关注每学年开始的 ACM 校队招新喵!
到这个时候了还缺队友实在不妙!求求求求快来个大神带我呜呜呜呜
1011
签到。
写了个状压 bfs 打了下表,发现当 时答案为 ,否则为 。
证明有点恶心,详见官方题解。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const LL p = 998244353; //============================================================= // bool yes[1000010]; //============================================================= // void init() { // int all = (1 << n) - 1; // for (int i = 0; i <= all; ++ i) yes[i] = 0; // } // void solve() { // std::queue<int> q; q.push(0); // while (!q.empty()) { // int u = q.front(); q.pop(); // yes[u] = 1; // for (int i = 0; i < n; ++ i) { // int next = u; // if (i - 1 >= 0) next ^= (1 << (i - 1)); // next ^= (1 << i); // if (i + 1 < n) next ^= (1 << (i + 1)); // if (!yes[next]) q.push(next); // } // } // } LL qpow(LL x_, LL y_) { LL ret = 1ll; while (y_) { if (y_ & 1) ret = ret * x_ % p; x_ = x_ * x_ % p, y_ >>= 1ll; } return ret; } //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { int n; std::cin >> n; LL ans = 0; if (n % 3 == 0) ans = qpow(2ll, n); else ans = qpow(2ll, n - n % 3 + 1); std::cout << ans << "\n"; } // for (int i = 1; i <= 15; ++ i) { // n = i; // init(); // solve(); // int cnt = 0, all = (1 << n) - 1; // for (int i = 0; i <= all; ++ i) cnt += yes[i]; // std::cout << i << " " << cnt << "\n";; // } return 0; }
1013
签到。
大神 dztlb 看了一眼秒了。
设当前距离终点 。
位于 0 直达终点概率为 。
若第一次没有直达终点,则之后一定均会停留在 上。此时每次随机到 直达终点的概率也仍为 ;若随机到了 则一定不会直达终点,赠送的机会直达终点的概率为 ,则一次操作直达终点的概率为 ,否则还会停留在 ,则期望的额外代价为
则答案即为:
#include <bits/stdc++.h> using namespace std; #define int long long const int mod=1e9+7; int qpow(int a,int b){ int ans=1; while(b){ if(b&1) ans=ans*a%mod; b>>=1; a=a*a%mod; } return ans; } int n,T; signed main(){ cin>>T; while(T--){ cin>>n; cout<<((n-1)+mod+qpow(n,mod-2))%mod<<'\n'; } return 0; }
1006
博弈。
手玩出来 均为奇数的情况下先手必败,有奇数有偶数先手必胜。若全是偶数则可以令所有数不断除 2 直至出现有奇数为止。
原因是奇奇偶和奇偶偶的局面都能转移到奇奇奇,而奇奇奇的局面只能转移到奇奇偶。又最终局面为 111,则若先手奇奇奇,则后手可以一直令自己手里的奇奇偶局面转移到奇奇奇直至 111,否则先手可以一直令后手拿到奇奇奇。
而对于偶偶偶局面,可以转移到偶偶偶和奇奇偶局面。由于奇奇偶是先手必胜态,则两方一定会不断地仅转移到偶偶偶直至无可奈何地出现奇数。
#include <bits/stdc++.h> using namespace std; #define int long long const int mod=1e9+7; int qpow(int a,int b){ int ans=1; while(b){ if(b&1) ans=ans*a%mod; b>>=1; a=a*a%mod; } return ans; } int n,T,a,b,c; signed main(){ cin>>T; while(T--){ cin>>a>>b>>c; int cnt; while(1){ cnt=0; if(a%2==0){ ++cnt; } if(b%2==0){ ++cnt; } if(c%2==0){ ++cnt; } if(cnt==3){ a/=2,b/=2,c/=2; }else{ break; } } if(cnt>=1&&cnt<=2){ puts("YES"); }else puts("NO"); } return 0; }
1008
最小割。
典中典之最大权闭合子图模型,可见经典题:P1361 小M的作物。
发现给两棵树只是用来哈人的,给定的限制实际上形如:对于两个物品 ,若将他们同时放入第一个集合中可获得 的价值,同时放入第二个集合中可获得 的代价(),求将 划分到两个集合中的最大价值。
限制的数量是 级别的,于是转化为经典最大权闭合子图模型。由于选单个点没有代价,则可以建出如下的图:
- 对于第 个限制建立虚拟节点 分别代表将 放入第一棵树/第二棵树。
- 向 连边,容量为 ; 向 连边,容量为 。
- 向 连边,容量 , 向 连边,容量 。
在此时的图上跑最小割,容量 的边不会被割去,若割去 的边相当于将 同时放到第一棵树中,若割去 相当于将 同时放到第二棵树中。则最小割相当于划分到两个集合后最少会减少多少价值。
则答案即为所有限制价值的 之和减去最小割。
注意限制最多有 个,则虚拟点数是 级别的。
//知识点:网络最大流,Dinic /* By:Luckyblock */ #include <bits/stdc++.h> #define pii std::pair<int,int> #define mp std::make_pair const int kN = 5010; const int kM = 1e6 + 10; const int kInf = 1e9 + 2077; //============================================================= int n, m, k, S, T; bool noisy[kN]; int nodenum, edgenum = 1, v[kM], ne[kM], head[kN]; int cur[kN], dep[kN]; int ans, w[kM]; //============================================================= void Add(int u_, int v_, int w_) { v[++ edgenum] = v_; w[edgenum] = w_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Init() { std::cin >> n >> k; ans = 0; edgenum = 1; nodenum = n; S = ++ nodenum, T = ++ nodenum; for (int i = 1; i <= n + 4 * k + 2; ++ i) { noisy[i] = 0; head[i] = 0; } for (int i = 1; i <= k; ++ i) { int x; std::cin >> x; noisy[x] = 1; } std::map<pii, int> haveedge, w1, w2; for (int i = 1; i < n; ++ i) { int u_, v_, w_; std::cin >> u_ >> v_ >> w_; if (!noisy[u_] || !noisy[v_]) continue; if (u_ > v_) std::swap(u_, v_); haveedge[mp(u_, v_)] = 1; w1[mp(u_, v_)] = w_; ans += w_; } for (int i = 1; i < n; ++ i) { int u_, v_, w_; std::cin >> u_ >> v_ >> w_; if (!noisy[u_] || !noisy[v_]) continue; if (u_ > v_) std::swap(u_, v_); haveedge[mp(u_, v_)] = haveedge[mp(u_, v_)] + 2; w2[mp(u_, v_)] = w_; ans += w_; } for (auto [e, v]: haveedge) { int w11 = 0, w22 = 0; if (v & 1) w11 = w1[e]; if (v & 2) w22 = w2[e]; ++ nodenum; Add(S, nodenum, w11), Add(nodenum, S, 0); Add(nodenum, e.first, kInf),Add(e.first, nodenum, 0); Add(nodenum, e.second, kInf), Add(e.second, nodenum, 0); ++ nodenum; Add(nodenum, T, w22), Add(T, nodenum, 0); Add(e.first, nodenum, kInf), Add(nodenum, e.first, 0); Add(e.second, nodenum, kInf), Add(nodenum, e.second, 0); } // for (int i = 1; i <= nodenum; ++ i) { // for (int j = head[i]; j; j = ne[j]) { // if (w[j] == 0) continue; // std::cout << i << " " << v[j] << " " << w[j] << "\n"; // } // } } bool BFS() { std::queue <int> q; memset(dep, 0, (nodenum + 1) * sizeof (int)); dep[S] = 1; //注意初始化 q.push(S); while (!q.empty()) { int u_ = q.front(); q.pop(); for (int i = head[u_]; i > 1; i = ne[i]) { int v_ = v[i], w_ = w[i]; if (w_ > 0 && !dep[v_]) { dep[v_] = dep[u_] + 1; q.push(v_); } } } return dep[T]; } int DFS1(int u_, int into_) { if (u_ == T) return into_; int ret = 0; for (int i = cur[u_]; i > 1 && into_; i = ne[i]) { int v_ = v[i]; int w_ = w[i]; if (w_ && dep[v_] == dep[u_] + 1) { int dist = DFS1(v_, std::min(into_, w_)); if (!dist) dep[v_] = kN; into_ -= dist; ret += dist; w[i] -= dist, w[i ^ 1] += dist; if (!into_) return ret; } } if (!ret) dep[u_] = 0; return ret; } int Dinic() { int ret = 0; while (BFS()) { memcpy(cur, head, (nodenum + 1) * sizeof (int)); ret += DFS1(S, kInf); } return ret; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int time; std::cin >> time; while (time --) { Init(); std::cout << (ans - Dinic()) << "\n"; } return 0; } /* 1 4 3 1 2 3 1 2 1 2 3 2 2 4 1 2 3 5 2 4 1 4 1 1 1 4 3 1 2 3 1 2 2 2 3 1 2 4 1 2 3 2 2 4 1 4 1 1 */
1002
结论,暴力
发现一定可以对最小值做一次加操作使其变成某个权值 +1,此时再取模即可人为造出一个 1 来,再来 次操作即可达到目标,则操作次数上界为 次;所有数均为正数,考虑到一次取模操作最多将一个数变为 0,则操作次数下界为 ,且发现仅有所有数均为最小值的倍数时可以达到这个下界。
于是仅需检查给定局面是否通过 次操作达到目标。由上述讨论,这 次操作中最后 次一定为对 个数的取模操作,多出一次操作的结果一定是制造出了所有数的一个约数,于是仅需大力讨论多出的一次操作的类型:
- 若为取模操作,由于数据范围不大,直接大力 枚举能否造出所有数的一个约数即可。
- 若为加操作,则一定是对最小值操作使其变成所有数的 。
数据范围很小大力实现即可。
赛时不是我写的哈哈。
#include <bits/stdc++.h> using namespace std; #define int long long const int N=1005; int n,T,a[N]; int b[N],tot,ans,nn; signed main(){ cin>>T; while(T--){ tot=0; cin>>n; nn=n; ans=n+1; for(int i=1;i<=n;++i){ cin>>a[i]; b[i]=a[i]; } sort(a+1,a+1+n); a[0]=0; for(int i=1;i<=n;++i){ if(a[i]!=a[i-1]){ b[++tot]=a[i]; } } n=tot; for(int i=1;i<=n;++i) a[i]=b[i]; bool fl=1; int cnt=0; for(int i=2;i<=n;++i){ if(a[i]%a[1]!=0) fl=0,++cnt; } ans=min(ans,nn-1+cnt); if(fl) ans=min(ans,nn-1); for(int i=1;i<n;++i){ for(int j=i+1;j<=n;++j){ int k=a[j]%a[i]; if(k!=0){ for(int o=1;o<=n;++o) b[o]=a[o]; b[j]=k; sort(b+1,b+1+n); fl=1; for(int o=2;o<=n;++o){ if(b[o]%b[1]!=0) fl=0; } if(fl) ans=min(ans,nn); } } } for(int k=a[1]+1;k<=a[2];++k){ fl=1; for(int i=2;i<=n;++i){ if(a[i]%k!=0) fl=0; } if(fl) ans=min(ans,nn); } for(int i=n;i>=1;--i){ for(int j=1;j<i;++j){ if(a[i]%a[j]==0) a[i]=0; } } sort(a+1,a+1+n); tot=0; for(int i=1;i<=n;++i){ if(a[i]) b[++tot]=a[i]; } n=tot; for(int i=1;i<=n;++i) {a[i]=b[i];} fl=1; cnt=0; for(int i=2;i<=n;++i){ if(a[i]%a[1]!=0) fl=0,++cnt; } ans=min(ans,nn-1+cnt); if(fl) ans=min(ans,nn-1); for(int i=1;i<n;++i){ for(int j=i+1;j<=n;++j){ int k=a[j]%a[i]; if(k!=0){ for(int o=1;o<=n;++o) b[o]=a[o]; b[j]=k; sort(b+1,b+1+n); fl=1; for(int o=2;o<=n;++o){ if(b[o]%b[1]!=0) fl=0; } if(fl) ans=min(ans,nn); } } } for(int k=a[1]+1;k<=a[2];++k){ fl=1; for(int i=2;i<=n;++i){ if(a[i]%k!=0) fl=0; } if(fl) ans=min(ans,nn); } cout<<ans<<'\n'; } return 0; } /* 5 4 2 3 5 7 4 2 4 6 8 5 2 4 6 7 8 5 6 7 8 9 10 5 4 10 15 32 64 */
1004
树,数据结构
唉两个人还是难搞,在 dztlb 还在大战 1002 的时候猜到了 的数对不会很多这个结论,但是最后两个人都去攻 1005 了所以没时间写再来个人这题肯定也过了呃呃。
发现在满足 时,数对 的数量并不会很多,由题解可知是 级别的,于是考虑预处理出所有数对并将他们按 升序分类。则对于每个询问 ,会对其产生影响的点对为上述预处理后的点对序列的一段前缀,且仅有在 的子树中的点对会有影响。于是考虑离线所有询问,升序枚举 ,在此过程中不断加入点对的贡献并回答询问。
点对 在子树 中等价于 在子树 中,且若 对答案影响为 1,否则为 2。子树问题可用 dfs 序转成序列上的区间问题,于是考虑预处理所有子树的 dfs 序的范围 ,对于枚举的所有点对在 对应 dfs 序上位置上做单点加,查询 的答案等价于查询区间 之和,使用树状数组维护单点加区间和即可。
点对总数为 级别,为了保证总时间复杂度,求 应当使用 RMQ 的 的方法。则总时间复杂度为 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define pii std::pair<int,int> #define mp std::make_pair #define LL long long const int kN = 1e5 + 10; const int kQ = 1e6 + 10; const int kLim = 1e5; //============================================================= int n, q, edgenum, head[kN], v[kN << 1], ne[kN << 1]; int fa[kN], sz[kN], dep[kN], son[kN], top[kN]; int dfnnum, L[kN], R[kN]; std::vector<pii> lcms[kN], query[kN]; int ans[kQ]; //============================================================= void init() { for (int d = 1; d <= kLim; ++ d) { for (int i = d; i <= kLim; i += d) { for (int j = d; i / d * j <= kLim && j <= i; j += d) { if (std::__gcd(i, j) == d) lcms[i / d * j].push_back(mp(j, i)); } } } } void addedge(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } namespace ST { int num, Log2[kN << 1], f[kN << 1][21], fir[kN]; void Dfs(int u_, int fa_) { dep[u_] = dep[fa_] + 1; fir[u_] = ++ num; f[num][0] = u_; L[u_] = ++ dfnnum; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue ; Dfs(v_, u_); f[++ num][0] = u_; } R[u_] = dfnnum; } void Prepare() { num = 0; Dfs(1, 0); Log2[1] = 0; for (int i = 2; i <= num; ++ i) Log2[i] = Log2[i >> 1] + 1; for (int i = 1; i <= 20; ++ i) { for (int j = 1; j + (1 << i) - 1 <= num; ++ j) { if (dep[f[j][i - 1]] < dep[f[j + (1 << (i - 1))][i - 1]]) { f[j][i] = f[j][i - 1]; } else { f[j][i] = f[j + (1 << (i - 1))][i - 1]; } } } } int Lca(int u_, int v_) { int l = fir[u_], r = fir[v_]; if (l > r) std::swap(l, r); int lth = Log2[r - l + 1]; if (dep[f[l][lth]] < dep[f[r - (1 << lth) + 1][lth]]) return f[l][lth]; return f[r - (1 << lth) + 1][lth]; } } namespace BIT { #define lowbit(x) ((x)&(-x)) const int kL = kN; int lim, f[kN]; void Init(int n_) { lim = n_; for (int i = 1; i <= n_; ++ i) f[i] = 0; } void Insert(int pos_, int val_) { for (int i = pos_; i <= lim; i += lowbit(i)) f[i] += val_; } int Sum(int pos_) { int ret = 0; for (int i = pos_; i; i -= lowbit(i)) ret += f[i]; return ret; } int Query(int l_, int r_) { if (l_ > r_) return 0; return Sum(r_) - Sum(l_ - 1); } #undef lowbit } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); init(); int T; std::cin >> T; while (T --) { std::cin >> n; edgenum = dfnnum = 0; for (int i = 1; i <= n; ++ i) head[i] = 0; for (int i = 1; i <= kLim; ++ i) query[i].clear(); for (int i = 1; i < n; ++ i) { int u_, v_; std::cin >> u_ >> v_; addedge(u_, v_), addedge(v_, u_); } ST::Prepare(); BIT::Init(n); std::cin >> q; for (int i = 1; i <= q; ++ i) { int r, x; std::cin >> r >> x; query[x].push_back(mp(r, i)); } for (int x = 1; x <= kLim; ++ x) { for (auto [i, j]: lcms[x]) if (j <= n) BIT::Insert(L[ST::Lca(i, j)], 1 + (i != j)); for (auto [r, i]: query[x]) ans[i] = BIT::Query(L[r], R[r]); } for (int i = 1; i <= q; ++ i) std::cout << ans[i] << " "; std::cout << "\n"; } return 0; }
1010
树,倍增。
场上太长而且手里有不少能做的题就没看呃呃,实际也是可做题。
1005
树,调和级数,并查集,小结论大赏
小清新数据结构题呃呃,一开始没想到很关键的结论大力写了个 的树上 map
启发式合并预处理就 T 了呃呃呃呃,然后最后 30min 莫名奇妙就写完了一发过了哈哈我都不知道自己写了啥反正过了太爽
一个显然的想法是考虑降序枚举作为答案的 ,调和级数地枚举所有编号为 的倍数的节点 ,记该点集为 , 中所有点两两相连可以得到一棵子树,则断开该子树上任意一条边的答案不会小于 ,对于所有边仅需在上述过程中找到第一个覆盖它的子树即可找到它的答案。
然后考虑对于某个 对应的子树长什么样,发现求出 中所有点的 后,该子树即由 到所有 中节点的不相交路径构成。然后想到直接用树剖维护路径最大值,加上调和级数总时间复杂度直接变 级别了,数据范围太大铁过不去。
但是发现上述枚举答案下,每条边的答案可以且理应仅被更新一次,考虑直接枚举每棵子树上的边并大力更新。子树已经变成了以 为根的若干不交路径的形式,考虑枚举 中的所有点并大力上跳直到 即可。在此过程的路径中若有边的答案已经确定,则应当直接忽略它并飞到第一个没有被确定的边那里,想到使用并查集将以确定答案的边的两端点直接合并起来,以便于大力上跳。
于是在大力上跳回答询问的同时,对于所有点维护并查集,表示在询问过程中通过合并后所在连通块的深度最浅的节点,并维护每个点的父边的编号,大力上跳时边跳边合并,则可以保证每条边仅被枚举到一次。
赛时用了树剖求 ,总时间复杂度是很小常数的 级别,现在在 hduoj 上过不去,但是赛时 3.6s 卡过去了哈哈算我牛逼。
赛后发现实际上这个求 后的上跳过程都没有必要,仅需直接大力枚举树上路径,并大力并查集合并即可,也可以保证枚举到所有需要被更新答案的边。则省去了求 的环节,复杂度变为纯粹的调和级数 。写法详见官方题解。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e6 + 10; //============================================================= int n, m, ans[kN]; int edgenum, head[kN], id[kN << 1], v[kN << 1], ne[kN << 1]; int fa[kN], sz[kN], dep[kN], son[kN], top[kN]; int newfa[kN], newid[kN]; //============================================================= void Add(int u_, int v_, int id_) { v[++ edgenum] = v_; id[edgenum] = id_; ne[edgenum] = head[u_]; head[u_] = edgenum; } namespace Cut { void init() { for (int i = 1; i <= n; ++ i) son[i] = 0; } void Dfs1(int u_, int fa_) { sz[u_] = 1; fa[u_] = fa_; dep[u_] = dep[fa_] + 1; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; Dfs1(v_, u_); sz[u_] += sz[v_]; if (sz[v_] > sz[son[u_]]) son[u_] = v_; } } void Dfs2(int u_, int top_) { top[u_] = top_; if (son[u_]) Dfs2(son[u_], top_); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa[u_] || v_ == son[u_]) continue; Dfs2(v_, v_); } } int Lca(int u_, int v_) { for (; top[u_] != top[v_]; u_ = fa[top[u_]]) { if (dep[top[u_]] < dep[top[v_]]) std::swap(u_, v_); } return dep[u_] < dep[v_] ? u_ : v_; } } void Dfs(int u_, int fa_) { newfa[u_] = u_; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], id_ = id[i]; if (v_ == fa_) continue; newid[v_] = id_; Dfs(v_, u_); } } int find(int x_) { int ret = (newfa[x_] == x_) ? x_ : (newfa[x_] = find(newfa[x_])); newid[x_] = newid[ret]; return ret; } void merge(int x_, int y_) { int fx = find(x_), fy = find(y_); if (dep[fx] < dep[fy]) std::swap(fx, fy); if (fx == fy) return ; newfa[fx] = fy; newid[fx] = newid[fy]; } //============================================================= int main() { // freopen("1.txt", "r", stdin); int size(256<<20); //256M __asm__ ( "movq %0, %%rsp\n"::"r"((char*)malloc(size)+size)); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { std::cin >> n; edgenum = 0; for (int i = 1; i <= n; ++ i) head[i] = 0, ans[i] = 1, newid[i] = 0; Cut::init(); for (int i = 1; i < n; ++ i) { int u_, v_; std::cin >> u_ >> v_; Add(u_, v_, i), Add(v_, u_, i); } Cut::Dfs1(1, 0), Cut::Dfs2(1, 1); Dfs(1, 0); for (int d = n / 2; d >= 2; -- d) { int lca = d; for (int i = 2; d * i <= n; ++ i) lca = Cut::Lca(lca, d * i); for (int i = 1; d * i <= n; ++ i) { int v_ = d * i; for (int now = find(v_); now != find(lca); ) { if (newid[now] && ans[newid[now]] == 1) ans[newid[now]] = d; merge(now, find(fa[now])); now = find(now); } } } for (int i = 1; i < n; ++ i) std::cout << ans[i] << " "; std::cout << "\n"; } exit(0); return 0; } /* */
写在最后
结尾广告:中南大学 ACM 集训队绝赞招新中!
有信息奥赛基础,获得 NOIP 省一等奖并达到 Codeforces rating 1900+ 或同等水平及以上者,可以直接私聊我与校队队长联系,免选拔直接进校集训队参加区域赛!
没有达到该水平但有志于 XPCX 赛事请关注每学年开始的 ACM 校队招新喵!
到这个时候了还缺队友实在不妙!求求求求快来个大神带我呜呜呜呜
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)