今天是杨溢鑫老师的讲授~
T1
1 题意:
n * m 的地图,有 4 种不同的地形(包括空地),6 种不同的指令,求从起点及初始的状态开始根据指令行动的结果。
2 思路:(虽然分了数据范围但是实际上思路没什么差别,所以直接讲 100%的思路了)
这个题除了题面又臭又长也没什么难点了,整体上是直接在线处理,然后就是针对不同的指令采取不同的处理措施:
“ FT x ” 和 ” WT x ” :这个指令只能进行转动,所以处理起来很轻松,一直记录炮台和机器人本体的方向,然后随着指令转动即可,唯一要检验的就是参数是否符合规则。
“ FF i ”:这个指令需要讨论一下,以下两种情况该指令不被执行: i == 0 且小水晶弹数量为 0、i == 1 且大水晶弹数量为 0;
以下两种情况该指令将导致停机并返回 ” ERROR ”:
参数错误、相应水晶弹数量不为 0 且弹夹已满;
其他情况则正常处理,记得记录一下弹丸的大小,发射的时候要用; “ FE ”:
这个命令不需要检查错误,主要是区分一下发射的是大 or 小水晶弹,前面有靶子的话就相应处理就完事了,没有水晶弹就啥都不干。
“ WG y ”:这个命令一是注意一下参数不能出错导致 ” ERROR ”,二是注意判断会不会撞到障碍和靶子或者撞进水池导致 ” ERROR ”,都没有问题的话就直接移动到目的点完事。
“ END ”:看到这个命令直接退出就行。
几个要注意的点:
a . 记得记录一下小机器人停机的情况,如果到最后都没有停过机则返回 ” ERROR ”;
b . 参数错误有可能是参数的大小不对,也有可能是参数的类型不对如(“ FT 0.3 ”)。 建议使用 getline 处理,总之处理参数也不需要多打几行……不过一般都不会被这种地方坑到的吧。
c . 记得出现过停机情况则之后的所有指令全部无视,所以记得全部读入一下别一不小心直接开始读下一组数据了……
d . 大概就这样,总之注意一下题目里的小细节,认真读题就没啥问题了。
#include <cstdio> #include <cstring> #include <iostream> using namespace std; const int xGo[4]={-1,0,1,0}; const int yGo[4]={0,-1,0,1}; int n,m,map[220][220][2]; int x,y,fOri,wOri,maxCilp,totCilp,cilp[32],bigBullet,smallBullet,k,endIf,totTarget; void IN_Map_Robot(); void OutPut(int); int Para(char str[]); void FortCom(char str[]); void WheelCom(char str[]); void EndCom(char str[]); int main() { freopen("robo.in","r",stdin); freopen("robo.out","w",stdout); int t; scanf("%d",&t); while(t--) { IN_Map_Robot(); while(k--) { char str[10]; cin.getline(str,10); if(endIf) continue; if(str[0]=='F') FortCom(str); if(str[0]=='W') WheelCom(str); if(str[0]=='E') EndCom(str); } if(!endIf) OutPut(0); } fclose(stdin); fclose(stdout); return 0; } void IN_Map_Robot() { memset(map,0,sizeof(map)); totCilp=0,fOri=0,wOri=0,endIf=0,totTarget=0; scanf("%d%d",&n,&m); for(int i=0;i<n;++i) for(int j=0;j<m;++j) scanf("%d ",&map[i][j][0]); scanf("%d %d %d %d %d %d\n",&x,&y,&maxCilp,&bigBullet,&smallBullet,&k); } void FortCom(char str[]) { if(str[1]=='T') { int par=Para(str+3); if(par==0) fOri=(fOri+1)%4; else if(par==1) fOri=(fOri+3)%4; else { OutPut(0); return ; } } if(str[1]=='F') { int par=Para(str+3); if((par==0&&smallBullet==0)||(par==1&&bigBullet==0)) return; if(par==0&&totCilp<maxCilp) cilp[++totCilp]=par,smallBullet--; else if(par==1&&totCilp<maxCilp) cilp[++totCilp]=par,bigBullet--; else { OutPut(0); return ; } } if(str[1]=='E') { if(totCilp==0) return ; int nx,ny; for(int i=1;;++i) { nx=x+xGo[fOri]*i,ny=y+yGo[fOri]*i; if(nx<0||nx>=n||ny<0||ny>=m) break; if(map[nx][ny][0]==1||map[nx][ny][0]==2) break; } totCilp--; if(nx<0||nx>=n||ny<0||ny>=m) return ; if(map[nx][ny][0]==1) return ; if(cilp[totCilp+1]||map[nx][ny][1]) { map[nx][ny][0]=0; totTarget++; } else map[nx][ny][1]=1; } } void WheelCom(char str[]) { if(str[1]=='T') { int par=Para(str+3); if(par==0) wOri=(wOri+1)%4; else if(par==1) wOri=(wOri+3)%4; else { OutPut(0); return ; } } else { int par=Para(str+3); int nx=x+xGo[wOri]*par,ny=y+yGo[wOri]*par; if(nx<0||nx>=n||ny<0||ny>=m) { OutPut(0); return ; } else { for(int i=1;i<=par;++i) { nx=x+xGo[wOri]*i,ny=y+yGo[wOri]*i; if(map[nx][ny][0]) { OutPut(0); return ; } } } x=nx,y=ny; } } void EndCom(char str[]) { OutPut(1); } void OutPut(int type) { endIf=1; if(type) printf("Complete\n"); else printf("ERROR\n"); printf("%d %d\n",x,y); printf("%d\n",totTarget); printf("%d %d %d %d\n",fOri,wOri,bigBullet,smallBullet); } int Para(char str[]) { int i,re=0; for(i=0;;++i) { if(str[i]=='.') return 114514; if(str[i]=='\0') break; } i=0; do { re=re*10+(str[i]-'0'); i++; }while(str[i]!='\0'); return re; }
T2
1 题意:
在一个 n * m 的网格图上,有 个目标点,一个体格为 s 的刺豚要从起点遍历每个目标点。 要求在路径最短的前提下保证每个点处的刺豚体格之和最大。
2 解题思路 :
2.1: 对于 30% 的数据 所有菜市位置和小 T 所在的位置在一条水平直线上,考虑有部分菜市在小 T 右边,部分菜市在小 T 左边。
只有两种遍历情况:向左再向右、向右再向左,而在每个点处的可行的最大体格是确定的,因此直接累加即可。 最终答案在两种情况中选取一个路径更短的或最短路径相同情况下体格和最大的。
2.2: 对于 s=0 且 n , m <= 50 的数据
当 s=0 时,问题转变为从一个点出发遍历 p 个点的最短路问题。
最朴素的想法应该是搜索,每次搜索下一步去哪个点。 但搜索的问题在于它考虑了经过每个点的顺序 。 实际上经过顺序不会对答案产生影响、经过了哪些点才会有影响。 因此考虑用状压表示哪些点没去过,然后寻找一个没去过的点更新(旅行商问题)。
定义状态:dis [ x ][ y ][ s ] 表示还没去过 s 集合中的点,目前在点 ( x , y ) 的最短距离。 可使用 bfs 更新,复杂度 O ( n * m * 2 ) ,可解决 n , m <= 50 的情况 。
2.3 对于s=0的数据
很容易可以发现, dis [ x ][ y ][ s ] 的三维中, s 在很多点是不会被更新的,因此这些点处出现了状态冗余。 因此只在 p 个点处定义 ,即状态改为 dis [ i ][ s ] ,表示在第 i 个点处的最短路。
转移时需要利用两个点间的最短路。
最短路可通过 O ( p ) 次 bfs 预处理得到。 复杂度 O ( p * n * m + 2 * p )
2.4 对于 p=1 的数据
只有一个目标点,因此只用考虑膨胀问题。
在每一个点都会重新膨胀,因此在每个点处的最大膨胀值其实是固定的。
在 bfs 的时候直接将每个点的膨胀值加入答案即可。 复杂度 O ( p * n * m + s * n * m )
2.5 对于100%的数据
将 2.3 中的 bfs 部分与 2.4 结合,即可得到正解。 复杂度 O ( ( p + s ) * n * m + 2 * p )
#include <bits/stdc++.h> using namespace std; const int N=505,M=505,K=15; const int dx[4]={0,0,1,-1}; const int dy[4]={1,-1,0,0}; const char* st[4]={"R","L","D","U"}; int Path[(N+M)<<1],n,m,S; int mp[N][M]; void Read_Map() { scanf("%d%d%d",&n,&m,&S); for (int i=0;i<n;++i) for (int j=0;j<m;++j) scanf("%d",&mp[i][j]);//mp=0为可行,否则为障碍 } queue<pair<int,int> >Q; int Path_[K+1][K+1][(N+M)<<1],dist[K+1][K+1],szt[K+1][K+1]; int dis[K][1<<K],sum[K][1<<K]; pair<int,int> pre[K][1<<K]; void Put_Path(int *Path,int &len,int begin,int end) { for (int i=0;i<dist[begin][end];++i) Path[len++]=Path_[begin][end][i]; } bool CoordValid(int x,int y) { return x>=0 && x<n && y>=0 && y<m; } bool CoordValid(int x,int y,int size) { for (int i=-size;i<=size;++i) for (int j=-size;j<=size;++j) if (!CoordValid(x+i,y+j) || mp[x+i][y+j]==1) return 0; return 1; } int Size(int x,int y) { for (int i=S;i>=0;--i) if (CoordValid(x,y,i)) return i; return -1; } int pree[N][M],diss[N][M],sz[N][M]; void FindPath(int *ax,int *ay,int count) { for (int i=0;i<count;++i) { Q.push(make_pair(ax[i],ay[i])); memset(pree,0,sizeof(pree)); memset(diss,0x7f,sizeof(diss)); memset(sz,0,sizeof(sz)); diss[ax[i]][ay[i]]=0; while (!Q.empty()) { pair<int,int> u=Q.front(); Q.pop(); for (int i=0;i<4;++i) { int tx=u.first+dx[i],ty=u.second+dy[i],s=Size(tx,ty); if (s!=-1 && (diss[tx][ty]>diss[u.first][u.second]+1 || (diss[tx][ty]==diss[u.first][u.second]+1 && sz[u.first][u.second]+s>sz[tx][ty]))) { diss[tx][ty]=diss[u.first][u.second]+1; pree[tx][ty]=i; sz[tx][ty]=sz[u.first][u.second]+s; Q.push(make_pair(tx,ty)); } } } for (int j=0;j<count;++j) { dist[i][j]=diss[ax[j]][ay[j]]; szt[i][j]=sz[ax[j]][ay[j]]; // int len=0,nowx=ax[j],nowy=ay[j]; // while (nowx!=ax[i] || nowy!=ay[i]) { // Path_[i][j][len++]=pree[nowx][nowy]; // int px=nowx-dx[pree[nowx][nowy]],py=nowy-dy[pree[nowx][nowy]]; // nowx=px; nowy=py; // } // reverse(Path_[i][j],Path_[i][j]+len); } } } void Planning(int now_x,int now_y,int *aim_x,int *aim_y,int count_aim,int *Path,int &len) { aim_x[count_aim]=now_x; aim_y[count_aim]=now_y; FindPath(aim_x,aim_y,count_aim+1); int Mx=1<<count_aim; for (int i=0;i<=count_aim;++i) for (int j=0;j<Mx;++j) pre[i][j]=make_pair(-1,-1),dis[i][j]=1<<30; for (int i=0;i<count_aim;++i) dis[i][(Mx-1)^(1<<i)]=0,sum[i][(Mx-1)^(1<<i)]=0; for (int i=0;i<count_aim;++i) for (int j=Mx-1;j>=0;--j) for (int k=0;k<count_aim;++k) if (i!=k && !((j>>k)&1) && (dis[i][j]>dis[k][j^(1<<i)]+dist[i][k] || ( dis[i][j]==dis[k][j^(1<<i)]+dist[i][k] && sum[i][j]<sum[k][j^(1<<i)]+szt[i][k] ) )) { dis[i][j]=dis[k][j^(1<<i)]+dist[i][k]; sum[i][j]=sum[k][j^(1<<i)]+szt[i][k]; pre[i][j]=make_pair(k,j^(1<<i)); } int mn=0,v=0; len=0; for (int i=1;i<count_aim;++i) if (dis[i][v]+dist[count_aim][i]<dis[mn][v]+dist[count_aim][mn] || (dis[i][v]+dist[count_aim][i]==dis[mn][v]+dist[count_aim][mn] && sum[i][v]+szt[count_aim][i]>sum[mn][v]+szt[count_aim][mn])) mn=i; printf("%d %d\n",dis[mn][v]+dist[count_aim][mn],sum[mn][v]+Size(now_x,now_y)+szt[count_aim][mn]); /* for (int i=1;i<count_aim;++i) if (dis[i][v]+dist[count_aim][i]<dis[mn][v]+dist[count_aim][mn]) mn=i; Put_Path(Path,len,count_aim,mn); while (pre[mn][v].first!=-1) { pair<int,int> u=pre[mn][v]; Put_Path(Path,len,mn,u.first); mn=u.first; v=u.second; } */ } int main() { freopen("expand.in","r",stdin); freopen("expand.out","w",stdout); Read_Map(); int now_x,now_y,aim_x[K],aim_y[K],count_aim; scanf("%d%d%d",&now_x,&now_y,&count_aim); for (int i=0;i<count_aim;++i) scanf("%d%d",&aim_x[i],&aim_y[i]); int len=0; Planning(now_x,now_y,aim_x,aim_y,count_aim,Path,len); // for (int i=0;i<len;++i) // printf("%s",st[Path[i]]); putchar('\n'); fclose(stdin); fclose(stdout); return 0; } /* input: 4 6 3 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 1 3 0 output: 7 5 */
T3
对于 10%只有操作 2
直接不输出即可。(没拿到 10 分的我可得自我批评了)
对于另外 20%的数据,保证 r - l + 1 <= 7
首先我们注意到每次操作一个区间最多 7 个元素,我们对于操作 1 可以用搜索来枚举是否最终总价值会变为 0。对于每个数的系数,有三种可能 1,0,-1。1 表示选入上午,0 表示不参加,-1 表示选入下午。
如果最终价值变为 0 即可提前结束搜索。每次最坏复杂度 3。对于操作 2,暴力修改即可。
对于另外 30%的数据,保证只有操作 1 设 r - l + 1 = len。则子集合方案数为 2,由于一共有 len 个数,每个数的最大值为 v,所以区间值域为 [ len,len * v ] 。
利用抽屉原理,只要使 2 > len * v ,就保证操作 1 一定会输出 Yes 。(如果同一个小孩被同时包含在两个集合内,那就让两个集合都减去这个小孩,它们的和还是相当的)
将 v = 1000 代入:2 > len * 1000, 解得 len 的最小正整数解为 14。所以当 len >= 14,操作 1 可以直接输出 Yes。
当 len < 14:首先会想到像上一个分段一样搜索,但每次搜索复杂度最大为 3。 考虑采用二分优化,首先搜索完 [ l,mid ] 内所有集合的可能值域,再去搜索 [ mid + 1,r ] ,一旦发现得到的某个值在之前出现过(搜到了两种相同的方案,且此时它们的系数相同),即可直接结束搜索;如果发现得到的某个值是之前出现过的某个数的相反数,也可以结束搜索(系数不相同,但是绝对值相同) ;当然如果两个搜索区间内搜索到值为 0 也可提前结束搜索。
每次搜索复杂度最坏为 3 + 3
对于 100%的数据
对于操作 2,考虑用线段树 lazy-tag 实现区间修改。
区间幂不好区间修改,但考虑到每次最多调用 13 个数,可以下放到叶子节点时才释放 lazy 标记。 lazy 标记释放时,快速乘或暴力修改常数是巨大的。发现 b [ i ] 值域在 [ 0,v - 1 ],可以提前处理好 [ 0,v - 1 ] 的幂的表格,用倍增实现:
data [ i ][ 0 ] = i % v,data [ i ][ j ] = data [ data [ i ][ j - 1 ] ][ j - 1 ] 。
# include <iostream> # include <cstdio> # include <cstring> # include <cstdlib> using namespace std; const int V = 1e3 + 10; const int N = 1e5 + 12; int read() { int ans = 0,f = 1; char i = getchar(); while(i < '0' || i > '9'){if(i == '-')f = -1;i = getchar();} while(i >= '0' && i <= '9'){ans = ans * 10 + i - '0';i = getchar();} return ans * f; } int n,m,v,mid,ol,x,y,d; int data[V][22],a[N],stack[N],cnt; int sum[N << 2],lazy[N << 2]; bool flag[N]; void down(int rt){ lazy[rt << 1] += lazy[rt]; lazy[rt << 1 | 1] += lazy[rt]; lazy[rt] = 0; } void push(int &ans,int &r){ int j = 20; while(j >= 0){ if(r >= (1 << j)){ ans = data[ans][j]; r -= (1 << j); if(r == 0)return; } j--; } } void updata(int L,int R,int l,int r,int rt){ if(L <= l && r <= R){ lazy[rt]++; return; } if(lazy[rt])down(rt); int mid = (l + r) >> 1; if(L <= mid)updata(L,R,l,mid,rt << 1); if(R > mid)updata(L,R,mid + 1,r,rt << 1 | 1); return; } void Query(int L,int l,int r,int rt){ if(l == r){ push(a[L],lazy[rt]); return; } if(lazy[rt])down(rt); int mid = (l + r) >> 1; if(L <= mid)Query(L,l,mid,rt << 1); else Query(L,mid + 1,r,rt << 1 | 1); return; } void init(){ for(int i = 0;i < v;i++){ data[i][0] = (i * i % v) * i % v; } for(int j = 1;j <= 20;j++){ for(int i = 0;i < v;i++){ data[i][j] = data[data[i][j - 1]][j - 1]; } } } void dfsl(int u,int dis,bool k){ if(ol)return; if(u == mid + 1){ if(k){ if(!dis){ ol = true; }else if(dis >= 0 && !flag[dis]){flag[dis] = true;stack[++cnt] = dis;} } return; } dfsl(u + 1,dis,k); dfsl(u + 1,dis + a[u] + 1,true); dfsl(u + 1,dis - a[u] - 1,true); return; } void dfsr(int u,int dis,bool k){ if(ol)return; if(u == y + 1){ if(k){ if(!dis){ ol = true; }else if(dis >= 0 && flag[dis]){ ol = true; } } return; } dfsr(u + 1,dis,k); dfsr(u + 1,dis + a[u] + 1,true); dfsr(u + 1,dis - a[u] - 1,true); return; } int main(){ freopen("birthday.in","r",stdin); freopen("birthday.out","w",stdout); n = read(),m = read(),v = read(); for(int i = 1;i <= n;i++)a[i] = read(); init(); for(int i = 1;i <= m;i++){ d = read(),x = read(),y = read(); if(d == 2){ updata(x,y,1,n,1); }else { if(y - x >= 13){ puts("Yes"); }else { for(int j = x;j <= y;j++){ Query(j,1,n,1); } mid = (x + y) >> 1; ol = false;cnt = 0; dfsl(x,0,false); dfsr(mid + 1,0,false); for(int i = 1;i <= cnt;i++){ flag[stack[i]] = false; } if(ol)puts("Yes");else puts("No"); } } } fclose(stdin); fclose(stdout); return 0; }
搜索与枚举
一.深度优先搜索
通过搜索得到一棵树形图
策略:只要能发现没走过的点,就走到它。有多个点可走就随便挑一个,如果无路可走就回退,再看有没有没走过的点可走。
在图上寻找路径【少数可用最短路解决】
解决递归形式的问题
有后效性的选择问题
组合问题
状态可能很多,因此数据范围一般较小
1、状态表示;
2、剪枝;
剪枝的方法:
最优答案剪枝
记忆化剪枝
可行性剪枝
……
基本上枚举了很多的可能性;
所以数据范围比较小;
最大也就100;
最重要的就是状态的表示:
我现在在哪个点,之前经过的点是多少……
写好了状态转移会简单哦~
然后重点就是剪枝了,很多情况就是去考你的剪枝;
记忆化剪枝,我们之前如果已经到过了一个点,那就记录一下,下次直接访问就好了;
记忆化剪枝最多的就是记忆化搜索;
可行性剪枝:
如果一个点在当前不可行,那么在以后也一定不可行;
最大独立集问题;
如果是二分图直接网络流走起,可惜不是;
反图的概念:
在图中有的边在其对应反图中没有,在图中没有的边在其对应反图中有;
团的概念:
对于给定图 G = ( V , E ) 。其中,V = { 1 , … , n } 是图 G 的顶点集,E 是图 G 的边集。图 G 的团就是一个两两之间有边的顶点集合。简单地说,团是 G 的一个完全子图。如果一个团不被其他任一团所包含,即它不是其他任一团的真子集,则称该团为图 G 的极大团(maximal clique)。顶点最多的极大团,称之为图G的最大团(maximum clique)。最大团问题的目标就是要找到给定图的最大团。
团有一个很好的性质:不在同一团内的两点之间没有边相连,那么也就是说,不在同一个团里的点是不能同时存在的,只有在同一个团内的点才能同时存在;那么对于这个题,我们建完反图之后,就去找权值最大的团就可以了。
实现方法是搜索;
三个剪枝方法:
1. 删去不可能的点;
2. 利用可行点剪枝;
☆ 3. 当前答案来剪枝;
最大团主函数:
二.宽度优先搜索
queue<pair<int,int> > Q; int FindPath(pair<int,int> b,pair<int,int> e) { for (int i=0;i<n;++i) for (int j=0;j<m;++j) dis[i][j]=1e9+10; Q.push(b); dis[b.first][b.second]=0; while (!Q.empty()) { pair<int,int> u=Q.front(); Q.pop(); int x=u.first,y=u.second; for (int i=0;i<4;++i) { int tx=x+dx[i],ty=y+dy[i]; if (CoordValid(tx,ty) && mp[tx][ty]!=0 && dis[tx][ty]>dis[x][y]+1) { dis[tx][ty]=dis[x][y]+1; Q.push(make_pair(tx,ty)); } } } return dis[e.first][e.second]; }
记录当前素数的值;
每次选择一个位置,将其该改为另一个数;
检查新的数是否是素数;
为何不能直接 BFS ?
可以直接 BFS 求最短路的图边权只能是 1。
只有边权为 1 才能保证在所有前驱结点都被扩展以后再扩展当前点;
如何通过改变这个图使边权为 1?
拆点?将每个点拆成一个入点一个出点?
增加了长度为 0 的边,可能导致错误;
不能改变原有为 1 的边,如何特殊处理长度为 2 的边?
在到达 ‘x’ 点时,强制让当前点路径加 1,将 ‘x’ 改为 ‘@’,不扩展当前点,使当前点重新入队。
为何不会影响最终答案?
由于路径长度为1:
其他点在重新进行扩展到达当前点时,最短路长度≥当前最短路长度 +1;
因此其他点无法更新当前点答案,最终答案因此也不会改变;
代码实现(Python):
由于乘 2 这个跳法,使得我们不知道如何跳更优,那么我们直接 pass 深搜;
考虑用 bfs ,搜索所有的情况,取最优的;
时间复杂度:
对比 bfs 和 dfs
广搜一般用于状态表示比较简单求最优策略的问题;
优点:是一种完备策略,即只要问题有解,它就一定可以找到解。并且,广度优先搜索找到的解,还一定是路径最短的解;
缺点:盲目性较大,尤其是当目标节点距初始节点较远时,将产生许多无用的节点,因此其搜索效率较低。需要保存所有扩展出的状态,占用的空间大;
深搜几乎可以用于任何问题;
只需要保存 从起始状态到当前状态路径上的节点;
根据题目要求凭借自己的经验和对两个搜索的熟练程度做出选择;
三.枚举
选择连续的香蕉时最优;
枚举选择的香蕉起始位置,计算答案;
最大化最小值问题;
二分枚举四个角,如果可行,我们就去尝试枚举更大的值;
已知,在首位状态固定后,后续的操作是确定的。
只需要枚举首位是否按即可。
选择一个起点,暴力取枚举下一个点往哪跳,时间复杂度 O(5000)
我们发现青蛙是在直线上跳跃,且每次跳跃的距离都是相等的;
枚举每条路径后,排除错误答案;
按照 x 排序:
重识枚举
枚举:基于已知信息的猜测,从可能的答案集合中枚举并验证;
验证复杂度尽可能小;
枚举范围尽可能小(利用条件缩小枚举空间)
选择合理的枚举顺序(正序,倒序)
枚举什么?
怎么枚举?
怎么减少枚举?
四.二进制枚举
二进制枚举:
推导最后一行:
总结:
二进制的枚举一般用以枚举集合;
对集合的枚举涉及到不同的集合内部元素的选择;
枚举子集:
for(int S1=S;S1!=0;S1=(S1-1)&S) { S2=S^S1; //S2是另一个子集 }