2948 字
15 分钟
dp学习过程及记录

关键点#

  • dp的参数
  • dp的状态转移方程
  • 初始化
  • 细节部分

P1020 [NOIP1999 提高组] 导弹拦截#

Link

解析#

这道题分为两问,涉及动态规划及贪心思想

Question 1.#

根据题意,第一问求的即为最长不上升子序列。属于非常典型的dp。

O(N2)O(N^2) 做法#

参数:设 dpidp_i 表示以 aia_i 结尾的最长不上升子序列,最终答案即为 max1indpi\max_{1\le i \le n}dp_i

转移方程:dpi=max1j<i,ajai(dpi+1)dp_i=\max_{1\le j<i,a_j\le a_i}(dp_i+1)

复杂度:O(N2)O(N^2)

O(nlogn)O(n \log n) 做法#

nn 的范围扩大到 n105n \leq 10^5 时,第一种做法就不够快了,下面给出了一个 O(nlogn)O(n \log n) 的做法。

回顾一下之前的状态:(i,l)(i, l)

但这次,我们不是要按照相同的 ii 处理状态,而是直接判断合法的 (i,l)(i, l)

再看一下之前的转移:(j,l1)(i,l)(j, l - 1) \rightarrow (i, l),就可以判断某个 (i,l)(i, l) 是否合法。

初始时 (1,1)(1, 1) 肯定合法。

那么,只需要找到一个 ll 最大的合法的 (i,l)(i, l),就可以得到最终最长不下降子序列的长度了。


摘自OI wiki 最长不下降子序列

根据此思路,可以进行如下分析:

参数:dpidp_i 为所有的长度为 ii 的不下降子序列的末尾元素的最小值,lenlen 为子序列的长度。

做法:当前已知长度为 11,所以可从 22nn 循环,求出前 ii 个元素的最长长度,循环过程中不断更新维护 dpdp 数组及 lenlen 的长度。

如何维护?

考虑每一步的决策,

aidplena_i\le dp_len 时,显然 可以将其插入序列末尾,len+=1len+=1

反之,可以将 dpdp 序列中第一个大于它的元素更新为 aia_i。这一步中可以使用二分解决。


至此,Question 1.解决。

Question 2.#

第二问要求拦截所有导弹所需的最小数量。

考虑贪心#

从第一问来考虑,一套系统所拦截的即为当前的最长不上升子序列,推广考虑,多套系统拦截即为每次删除最长不上升子序列之后重复操作。所以根据Dilworth定理可得,最长上升子序列的长度就是能构成的不上升序列的个数

所以第二问实际上即为求解最长上升子序列的长度,算法与**Que 1.**基本相同,稍微改动即可。

Code#

// #pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define ONLINE_JUDGE
// #define MULTI_CASES
#define endl '\n'
const int MaxN = 2e5+100;
const int INF = 1e9;
const int mod=212370440130137957ll;
int T=1, N, M;
int a[MaxN];
int dp[MaxN],dp2[MaxN];
inline void Solve()
{
int x;
while(cin>>x){
a[++N]=x;
}
dp[1]=a[1];
int len=1;
for(int i=2;i<=N;i++){
if(a[i]<=dp[len]){
dp[++len]=a[i];
}
else{
int cnt=upper_bound(dp+1,dp+len+1,a[i],greater<int>())-dp;
dp[cnt]=a[i];
}
}
cout<<len<<endl;
// return;
len=1;
dp2[1]=a[1];
for(int i=2;i<=N;i++){
if(a[i]>dp2[len]){
len++;
dp2[len]=a[i];
}
else{
int cnt=lower_bound(dp2+1,dp2+len+1,a[i])-dp2;
dp2[cnt]=a[i];
}
}
cout<<len<<endl;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
//fclose(stdin);
//fclose(stdout);
return 0;
}

P1026 [NOIP2001 提高组] 统计单词个数#

解析#

本题求解将一个字符串拆分成 kk 个部分,使得每份中包含的单词个数总数最大。

数据很小,考虑 dpdp 做法。

首先,预处理 sumi,jsum_{i,j} 数组,表示从 iijj 的单词数。从后往前推,可得 sumi,j=sumi+1,jsum_{i,j}=sum_{i+1,j} 如果存在从 aia_i 开头的单词,则再加一(注意只加一次)。

关于参数,可以从 answeranswer 角度逆向去推,本题所求为分成 kk 份以后,包含单词的最大个数。通过简单推理可得,本题 dpdp 有三个参数,即为:

  • 最大个数
  • 分为 kk
  • 字符串长度 (这个条件没有明显指出,但十分关键,且在dp类做法中较为常见)

之后推理可得,dpi,jdp_{i,j} 表示一个长度为 ii 的字符串在拆分为 kk 份时,最多的单词个数。

考虑状态转移方程,设断点 r[j,i)r\in [j,i) 可以得出: dpi,j=maxjr<i(dpi,j,dpr,j1+sumr+1,i) dp_{i,j}=\max_{j\le r<i}(dp_{i,j},dp_{r,j-1}+sum_{r+1,i})

大体方向确定后,考虑初始化, dpi,j=sumi,i+dpi1,i1dp_{i,j}=sum_{i,i}+dp_{i-1,i-1} dpi,1=sum1,idp_{i,1}=sum_{1,i}

错误汇总#

RE on #3~#10:数组大小开为 kk,忽略了实际长度应该为 k×20k\times 20

Code#

// #pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define ONLINE_JUDGE
// #define MULTI_CASES
#define endl '\n'
const int MaxN = 800;
const int INF = 1e9;
const int mod=212370440130137957ll;
int T=1, N, M, K;
int a[MaxN];
string sa[10];
string s={},s1;
int sum[MaxN][MaxN];
bool check(int l,int r){
string x=s.substr(l,r-l+1);
for(int i=1;i<=K;i++){
if(x.find(sa[i])==0){
return true;
}
}
return false;
}
void init(){
for(int i=N;i>=1;i--){
for(int j=i;j>=1;j--){
sum[j][i]=sum[j+1][i];
if(check(j,i))sum[j][i]++;
}
}
}
int dp[MaxN][MaxN];
inline void Solve()
{
cin>>N>>M;
s+=' ';
for(int i=1;i<=N;i++){
cin>>s1;
s+=s1;
}
N*=20;
cin>>K;
for(int i=1;i<=K;i++){
cin>>sa[i];
}
init();
// dp[0][0]=sum[0][0];
dp[0][0]=0;
for(int i=1;i<=M;i++){
dp[i][i]=dp[i-1][i-1]+sum[i][i];
}
for(int i=1;i<=N;i++){
dp[i][1]=sum[1][i];
}
for(int i=1;i<=N;i++){
for(int j=1;j<=M&&j<i;j++){
for(int r=j;r<i;r++){
dp[i][j]=max(dp[i][j],dp[r][j-1]+sum[r+1][i]);
}
}
}
cout<<dp[N][M];
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
//fclose(stdin);
//fclose(stdout);
return 0;
}

P11187 配对序列#

解析#

根据题意,此题要求出序列中符合 c1,c1,c2,c2c_1,c_1,c_2,c_2 这种两两相同的最长子序列。

要求子序列最大值,显然我们可以使用 dpdp 做法。

考虑参数,设 dpidp_i 表示以 ii 为结尾的符合条件的子序列最大值。最终的答案即为 max1indpi\max_{1\le i\le n}{dp_i}

考虑预处理,我们可以先预处理出 preipre_i,表示序列中上一个与 aia_i 相同的数的位置。具体实现也十分简单,使用map即可。

不难得出,preipre_i 如果不为 00,则 dpidp_i 才会有值。又因为每个数字都是成对出现的,所以,只需要找到与这个数相同的上一个数上一个数的上一个数,求出从上一个数的上一个数的下一位至上一个数的前一位的最大值,这样可以满足 1i<k,s2is2i+11\le i<k,s2_i\ne s2_{i+1} 的条件。再加上当前的 22 就是 dpidp_i 的答案。

dpi=maxj=prepreiprei1dpj+2dp_i=\max_{j=pre_{pre_i}}^{pre_i-1}{dp_j+2}

关于求 max\max 的操作,我们可以使用数据结构优化,比如线段树或者ST表等。

注意如果 prei=0pre_i=0 则应该跳过。

Code#

// #pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define ONLINE_JUDGE
// #define MULTI_CASES
#define endl '\n'
const int MaxN = 5e5+100;
const int INF = 1e9;
const int mod=212370440130137957ll;
int T=1, N, M;
int a[MaxN];
int pre[MaxN];
struct Segment{
struct Point{
int l,r,tag;
int mex;
};
vector<int>a;
vector<Point>t;
Segment(int N){
a.assign(N+10,0);
t.assign(N<<4,{});
}
#define ls p<<1
#define rs p<<1|1
int midd(int p){
return (t[p].l+t[p].r)>>1;
}
void push_up(int p){
t[p].mex=max(t[ls].mex,t[rs].mex);
}
void change(int p,int k,int q){
int l=t[p].l,r=t[p].r;
if(l==r&&l==q){
t[p].mex+=k;
return;
}
int mid=midd(p);
if(q<=mid){
change(ls,k,q);
}
if(mid<q){
change(rs,k,q);
}
push_up(p);
}
void build(int p,int nl,int nr){
t[p].l=nl,t[p].r=nr;
if(nl==nr){
t[p].mex=a[nl];
return;
}
int mid=midd(p);
build(ls,nl,mid);
build(rs,mid+1,nr);
push_up(p);
}
int query(int p,int nl,int nr){
int l=t[p].l,r=t[p].r;
if(nl<=l&&r<=nr){
return t[p].mex;
}
int mid=midd(p);
int ans=0;
if(nl<=mid){
ans=max(ans,query(ls,nl,nr));
}
if(mid<nr){
ans=max(ans,query(rs,nl,nr));
}
return ans;
}
};
inline void Solve()
{
map<int,int>mp;
cin>>N;
Segment Tree(N);
for(int i=1;i<=N;i++){
cin>>a[i];
// Tree.a[i]=a[i];
if(mp[a[i]]==0){
mp[a[i]]=i;
pre[i]=0;
}
else{
pre[i]=mp[a[i]];
mp[a[i]]=i;
}
}
pre[0]=0;
Tree.build(1,1,N);
// Tree.change(1,1,1);
// cout<<1;return;
// vector<int>dp(N+1);
for(int i=1;i<=N;i++){
if(pre[i]==0)
continue;
int maxn=Tree.query(1,pre[pre[i]]+1,pre[i]-1);
// cout<<maxn<<" ";
Tree.change(1,maxn+2,i);
}
cout<<Tree.query(1,1,N);
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
//fclose(stdin);
//fclose(stdout);
return 0;
}

P11188 「KDOI-10」商店砍价#

解析#

此题中 vi105v_i\le 10^5 不难得出如果要花费 nn 的代价删除剩余的数,数位一定不能大于 66

由此可得,设 dpi,jdp_{i,j} 表示前 ii 位中已经删除 jj 个数时,删除所有数位的最小代价。

不难得出,每保留单独的一位,代价就会增加 10j1×sivi10^{j-1}\times s_i-v_i。所以我们可以从后往前递推,

可得 dpi,j=min(dpi,j,dpi,j+1+10j1×sivi)dp_{i,j}=\min(dp_{i,j},dp_{i,j+1}+10^{j-1}\times s_i-v_i)

显然,我们可以使用滚动数组来优化空间,所以最终的式子为: dpj=min(dpj,dpj+1+10j1×sivi)dp_j=\min(dp_j,dp_{j+1}+10^{j-1}\times s_i-v_i)

Code#

// #pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define ONLINE_JUDGE
#define MULTI_CASES
#define endl '\n'
const int MaxN = 2e5+100;
const int INF = 1e18;
const int mod=212370440130137957ll;
int T=1, N, M;
int a[MaxN];
int binpow(int a,int b){
if(b==0){
return 1;
}
int res=binpow(a,b/2);
if(b%2){
return res*res*a;
}
else{
return res*res;
}
}
inline void Solve()
{
string s;
cin>>s;
for(int i=1;i<=9;i++){
cin>>a[i];
}
int ans=0;
for(int i=0;i<s.size();i++)
{
ans+=a[s[i]-'0'];
}
int sum=ans;
// cout<<ans<<endl;
for(int i=6;i>=1;i--){
vector<int>dp(10);
dp.assign(10,INF);
dp[i]=sum;
for(int j=1;j<=s.size();j++){
for(int ii=1;ii<=i;ii++){
dp[ii]=min(dp[ii],dp[ii+1]+binpow(10,ii-1)*(s[j-1]-'0')-a[s[j-1]-'0']);
}
}
ans=min(ans,dp[1]);
// cout<<dp[1]<<' ';
}
cout<<ans<<endl;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>T;
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
//fclose(stdin);
//fclose(stdout);
return 0;
}

CF2025D Attribute Checks#

解析#

本题给定了两种属性,在过程中会获得 mm 个属性点,以及多次check,求使得通过的check数最多。

考虑 dpdp 做法,首先是 dpdp 的状态,设 dpi,jdp_{i,j} 表示获得 ii 个属性点,并将 jj 个属性点加在智力,将 iji-j 个属性点加在力量上时,能够通过的最多检查数。

考虑状态转移,显然可得:

对于 ai(1in)a_i(1\le i\le n),若 ai>0a_i>0dpcnt,j=maxj=aicnt(dpcnt1,j,dpcnt1,j1)dp_{cnt,j}=\max_{j=a_i}^{cnt}(dp_{cnt-1,j},dp_{cnt-1,j-1})ai<0a_i<0dpcnt,j=maxj=cntai0(dpcnt,j+1,dpcnt1,j+1)dp_{cnt,j}=\max_{j=cnt-\lvert a_i\rvert}^{0}(dp_{cnt,j}+1,dp_{cnt-1,j}+1)ai=0a_i=0dpcnt,j=dpcnt1,j(0j<cnt)dp_{cnt,j}=dp_{cnt-1,j}(0\le j<cnt)

考虑优化,可以发现,在cnt更新之后,只要check的值小于等于cnt,只需要将值加一即可。因此我们可以优化掉第一维只保留第二维。对于区间修改,可以使用线段树来实现。

Code#

TLE#

#pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define ONLINE_JUDGE
// #define MULTI_CASES
#define endl '\n'
const int MaxN = 2e6+100;
const int INF = 1e9;
const int mod=212370440130137957ll;
int T=1, N, M;
int a[MaxN];
int dp[5000][5000];
inline void Solve()
{
cin>>N>>M;
for(int i=1;i<=N;i++){
cin>>a[i];
}
int cnt=0;
for(int i=1;i<=N;i++){
if(a[i]==0){
cnt++;
for(int j=0;j<cnt;j++){
dp[cnt][j]=dp[cnt-1][j];
}
}
else{
if(a[i]>0){
for(int j=a[i];j<=cnt;j++){
dp[cnt][j]=max(dp[cnt][j]+1,dp[cnt-1][j-1]+1);
}
}
else{
for(int j=cnt-abs(a[i]);j>=0;j--){
dp[cnt][j]=max(dp[cnt][j]+1,dp[cnt-1][j]+1);
}
}
}
}
int ans=0;
for(int i=0;i<=cnt;i++){
ans=max(ans,dp[cnt][i]);
}
cout<<ans<<endl;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
//fclose(stdin);
//fclose(stdout);
return 0;
}

AC#

#pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define ONLINE_JUDGE
// #define MULTI_CASES
#define endl '\n'
const int MaxN = 2e6+100;
const int INF = 1e9;
const int mod=212370440130137957ll;
int T=1, N, M;
int a[MaxN];
// int dp[500][5000];
struct Segment{
vector<int>a;
struct Point{
int l,r,tag;
int mex;
};
vector<Point>t;
Segment(int N){
a.assign(N,0);
t.assign(N<<2,{});
}
#define ls p<<1
#define rs p<<1|1
int midd(int p){
return (t[p].l+t[p].r)>>1;
}
void push_up(int p){
t[p].mex=max(t[ls].mex,t[rs].mex);
}
void pls(int p,int k){
t[p].tag+=k;
t[p].mex+=k;
}
void push_down(int p){
if(t[p].tag){
pls(ls,t[p].tag);
pls(rs,t[p].tag);
t[p].tag=0;
}
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r){
t[p].mex=a[l];
return;
}
build(ls,l,midd(p));
build(rs,midd(p)+1,r);
push_up(p);
}
void dfs(int p,int l,int r){
if(l==r){
a[l]=t[p].mex;
return;
}
int mid=midd(p);
push_down(p);
dfs(ls,l,mid);
dfs(rs,mid+1,r);
push_up(p);
}
void change(int p,int nl,int nr,int k){
int l=t[p].l,r=t[p].r;
if(nl<=l&&r<=nr){
pls(p,k);
return;
}
push_down(p);
int mid=midd(p);
if(nl<=mid){
change(ls,nl,nr,k);
}
if(mid<nr){
change(rs,nl,nr,k);
}
push_up(p);
}
int query_max(int p,int nl,int nr){
int l=t[p].l,r=t[p].r;
if(nl<=l&&r<=nr){
return t[p].mex;
}
int mid=midd(p),ans=0;
push_down(p);
if(nl<=mid){
ans=max(ans,query_max(ls,nl,nr));
}
if(mid<nr){
ans=max(ans,query_max(rs,nl,nr));
}
return ans;
}
};
inline void Solve()
{
cin>>N>>M;
for(int i=1;i<=N;i++){
cin>>a[i];
}
int cnt=0;
Segment Tree(M+10);
Tree.build(1,0,M);
for(int i=1;i<=N;i++){
if(a[i]==0){
cnt++;
Tree.dfs(1,0,M);
for(int j=M;j>=1;j--)
Tree.a[j]=max(Tree.a[j],Tree.a[j-1]);
Tree.build(1,0,M);
}
else{
if(a[i]>0){
if(cnt>=a[i])
Tree.change(1,a[i],cnt,1);
}
else{
if(cnt>=abs(a[i]))
Tree.change(1,0,cnt-abs(a[i]),1);
}
}
}
// cerr<<cnt;return;
// cout<<1;return;
int ans=0;
cout<<Tree.query_max(1,0,cnt)<<endl;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
//fclose(stdin);
//fclose(stdout);
return 0;
}

P4059 找爸爸(daddy)#

题目分析#

题目要求DNA序列的最大相似程度,考虑 dpdp 做法,可以得出 dpi,j,kdp_{i,j,k} 表示 a序列长度为 ii,b序列长度为 jj ,末尾有/无空格(k=0k=0 无空格,k=1k=1 表示a序列有空格,k=2k=2 表示b序列末尾有空格)时的最大相似程度。

关于空格,对于每段连续空格,相似程度为 AB×(k1)-A-B\times (k-1) 因为 A,BA,B 都是正整数,所以其相似程度一定为负数。对于一个空格,会减少 AA 点相似度,如果在后面多添加一个空格,则会进一步减少 BB 点相似度。

状态转移方程如下:

dpi,j,0=max(dpi1,j1,0,dpi1,j1,1,dpi1,j1,2)+as1i,s2jdp_{i,j,0}=\max(dp_{i-1,j-1,0},dp_{i-1,j-1,1},dp_{i-1,j-1,2})+a_{s1_i,s2_j}

dpi,j,1=max(dpi1,j,0A,dpi1,j,1B,dpi1,j,2A)dp_{i,j,1}=\max(dp_{i-1,j,0}-A,dp_{i-1,j,1}-B,dp_{i-1,j,2}-A)

dpi,j,2=max(dpi,j1,0A,dpi,j1,1A,dpi,j1,2B)dp_{i,j,2}=\max(dp_{i,j-1,0}-A,dp_{i,j-1,1}-A,dp_{i,j-1,2}-B)

Code#

// #pragma GCC optimize(1, 2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
// #define ONLINEJUDGE
// #define MULTI_CASES
const int MaxN = 2e3 + 100;
const int INF = 1e18;
int T = 1, N, M;
int a[10][10];
int dp[MaxN][MaxN][4];
int A, B;
int g(int k)
{
return -A - B * (k - 1);
}
inline void Solve()
{
string s1, s2;
cin >> s1 >> s2;
s1 = ' ' + s1;
s2 = ' ' + s2;
map<char, int> mp;
mp['A'] = 1;
mp['T'] = 2;
mp['G'] = 3;
mp['C'] = 4;
for (int i = 1; i <= 4; i++)
{
for (int j = 1; j <= 4; j++)
{
cin >> a[i][j];
}
}
cin >> A >> B;
for (int i = max(s1.size(), s2.size()); i >= 1; i--)
{
dp[0][i][0] = dp[i][0][0] = dp[0][i][2] = dp[i][0][1] = dp[0][i][1] = dp[i][0][2] = -INF;
dp[0][i][1] = dp[i][0][2] = g(i);
}
for (int i = 1; i < s1.size(); i++)
{
for (int j = 1; j < s2.size(); j++)
{
dp[i][j][0] = max({dp[i - 1][j - 1][0], dp[i - 1][j - 1][1], dp[i - 1][j - 1][2]}) + a[mp[s1[i]]][mp[s2[j]]];
dp[i][j][1] = max({dp[i - 1][j][0] - A, dp[i - 1][j][1] - B, dp[i - 1][j][2] - A});
dp[i][j][2] = max({dp[i][j - 1][0] - A, dp[i][j - 1][1] - A, dp[i][j - 1][2] - B});
}
}
cout << max({dp[s1.size() - 1][s2.size() - 1][0], dp[s1.size() - 1][s2.size() - 1][1], dp[s1.size() - 1][s2.size() - 1][2]}) << endl;
}
signed main()
{
#ifndef ONLINEJUDGE
freopen("daddy.in", "r", stdin);
freopen("daddy.out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#ifdef MULTI_CASES
cin >> T;
while (T--)
#endif
Solve();
// fclose(stdin);
// fclose(stdout);
return 0;
}
dp学习过程及记录
https://blog.introl.top/posts/dp学习过程及记录/
作者
Introl
发布于
2024-10-10
许可协议
CC BY-NC-SA 4.0