本蒟蒻又来写blog~~er辣!!!
最近一直在练搜索学不懂啊啊啊。。。
发现枚举大多利用dfs解决,专属骗分技巧,题单上枚举题很多正解都是dfs呢。
要不总结总结????
→大脑cpu爆啦。。。
【算法1-3】暴力枚举 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/training/108
一,经典问题详解:
1.全排列:
例1:P1706: P1706 全排列问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
按照字典序输出自然数 11 到 n 所有不重复的排列,即n的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
输入:一个整数 n。
输出:由 1∼n 组成的所有不重复的数字序列,每行一个序列。每个数字保留 5 个场宽。
应该算是经典题了,
STL有一个很有用的函数next_permutation,用于求一个序列的下一个字典序排列
存在于头文件:
#include<algorithm>
基本用法:
next_permutaion(起始地址,末尾地址+1)
next_permutaion(起始地址,末尾地址+1,自定义排序)
默认升序,可自定义,和sort一样呢。
更改这个序列,让它成为下一个字典序排列,并返回一个 bool 值,表示是否成功生成下一个排列。当序列已经是字典序最大的排列时,不再存在下一个排列,函数将返回 false。
专门解决本题的函数。。。
利用STL解决本题很快呢:
#include <iostream>
#include<algorithm>
#include<vector>
#include<cstdio>
using namespace std;
int n;
int main()
{
cin>>n;
vector<int> a;
for(int i=1;i<=n;i++)
a.push_back(i);
do{
for(int i=0;i<a.size();i++)
printf("%5d ",a[i]);//注意格式
cout<<endl;
}while(next_permutation(a.begin(), a.end()));//生成排列,当生成字典序最大的排列时结束
return 0;
}
太简单了是吧qwq,如果蒟蒻不知道这个函数咋整?(next_permutation啥啊。。。)
分析一下:若我们一个一个枚举排列,有两点考虑。
1.枚举到何位置?
我们可以定义一个位置数组存放解,当位置到达上限时停止。
2.位置上放啥数字?
全排列数字要求不能重复,当选择了一个数字后加入位置数组后就不能再用了,于是我们可以定义一个状态数组判断是否使用,避免重复。
当然方案很多,这个状态数组排完一个方案后又要去找下一个方案,需要重置,这个就是回溯。
综上所述,设置几个变量
int n;//总数
bool book[100];//状态数组,标记数字是否使用,没使用为0,注意回溯
int a[100];//位置数组
主要计位置,位置为一个边界条件,利用位置判断返回or不返回
于是
void dfs(int step) //用来表示当前处理到了全排列的第几个位置
设位置从1开始,当位置到n+1时结束,输出结果。
//边界条件**********************************//
if(step==n+1)//这里说明前面的n个位置已经放好了,这是dfs结束的标志
{
for(int i=1;i<=n;i++)
printf("%5d",a[i]);//注意格式
printf("\n");
return;
/*
注意这个 return 它的作用不是返回主函数,而是返回上一级的dfs函数
例:如果此时是 dfs(5),遇到这个 return 就会回到上一级的 dfs函数
也就是dfs(4),但此时dfs(4)的大部分语句已经执行了,只需要接着执行 book[i]=0
然后继续进入for循环进入下一次的 dfs函数,直到结束。
*/
}
进入递归,依次枚举每个位置的数字,首先确定数字是否用过,没用过用了标记,递归完后复原。
//进行步骤*********************************//
for(int i=1;i<=n;i++)
{
if(book[i]==0)//如果未使用该数字
{
a[step]=i;//使用数字
book[i]=1;//标记已经使用
dfs(step+1);//进行下一个
book[i]=0;//回溯!!!!重点
}
}
return;//这里表示这一级别的dfs函数已经结束了,返回上一级 dfs函数
}
上总代码:qwq
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string>
using namespace std;
int n;
bool book[100];//标记数字是否使用,没使用为0,注意回溯
int a[100];//数据存入数组
void dfs(int step) //用来表示当前处理到了全排列的第几个位置
{
//边界条件**********************************//
if(step==n+1)//这里说明前面的n个位置已经放好了,这是dfs结束的标志
{
for(int i=1;i<=n;i++)
printf("%5d",a[i]);//注意格式
printf("\n");
return;
/*
注意这个 return 它的作用不是返回主函数,而是返回上一级的dfs函数
例:如果此时是 dfs(5),遇到这个 return 就会回到上一级的 dfs函数
也就是dfs(4),但此时dfs(4)的大部分语句已经执行了,只需要接着执行 book[i]=0
然后继续进入for循环进入下一次的 dfs函数,直到结束。
*/
}
//进行步骤*********************************//
for(int i=1;i<=n;i++)
{
if(book[i]==0)//如果未使用该数字
{
a[step]=i;//使用数字
book[i]=1;//标记已经使用
dfs(step+1);//进行下一个
book[i]=0;//回溯!!!!重点
}
}
return;//这里表示这一级别的dfs函数已经结束了,返回上一级 dfs函数
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
dfs(1); //进入dfs
return 0;
}
这里友好地中肯地附上伪代码,一定要细心理解!
//要理解dfs的关键在于解决当下该怎么做和下一步如何做
void dfs(int step)
{
判断边界
尝试每一种可能 for(i=1;i<=n;i++)
{
继续下一步 dfs(step+1)
}
返回
}
//基本模板放上哒
例2:P2089 烤鸡 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
让我想起了某位故人。。。。
这道题依然可以暴力枚举(嵌套的for实在是吓人qwq)
#include<cstdio>
#include<iostream>
using namespace std;
void g(int x)
{
int q=0;
int a,b,c,d,e,f,g,h,i,j;
for(a=1;a<=3;a++)
for(b=1;b<=3;b++)
for(c=1;c<=3;c++)
for(d=1;d<=3;d++)
for(e=1;e<=3;e++)
for(f=1;f<=3;f++)
for(g=1;g<=3;g++)
for(h=1;h<=3;h++)
for(i=1;i<=3;i++)
for(j=1;j<=3;j++)
{
if(a+b+c+d+e+f+g+h+i+j==x)
q++;
}
printf("%d\n",q);
}
void f(int x)
{
int a,b,c,d,e,f,g,h,i,j;
for(a=1;a<=3;a++)
for(b=1;b<=3;b++)
for(c=1;c<=3;c++)
for(d=1;d<=3;d++)
for(e=1;e<=3;e++)
for(f=1;f<=3;f++)
for(g=1;g<=3;g++)
for(h=1;h<=3;h++)
for(i=1;i<=3;i++)
for(j=1;j<=3;j++)
{
if(a+b+c+d+e+f+g+h+i+j==x)
printf("%d %d %d %d %d %d %d %d %d %d\n",a,b,c,d,e,f,g,h,i,j);
}
}
int main()
{
int a;
scanf("%d",&a);
if(a>30||a<10)
printf("0\n");
else
g(a);
f(a);
return 0;
}
呐呐,代码恐怖噻。。都嵌套了为啥不用搜索,递归搜索呐!(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )。。
首先是有10个位置放调料,总调料质量要刚好分配完。。。两个变量
于是dfs为
int n;//总质量
int kind=0;//记方案数量
int arr[10];//记方案
int m[10000][10];//存入方案全部
void dfs(int total,int a)//a为枚举到了哪个调料,total为已选调料的总质量
分析dfs的边界,满足10个位置且总质量分配好后完成一种方案
于是边界为
if(a==10)//边界条件
{
if (total==n)
{
for(int j=0;j<10;j++)
m[kind][j]=arr[j];//存入方案
kind++;//计数量
}
return;
}
if(total>n) return;//剪枝优化,当总质量超过限制时返回上一层
每个位置枚举质量,枚举要么1要么2要么3.。。
于是进入递归为
for(int i=1;i<=3;i++)
{
arr[a]=i;
dfs(total+i,a+1);//入递归
}
甭废话,上代码:
//依次枚举每个调料可以放多少
//指数级枚举
#include<iostream>
#include<cmath>
#include<string>
#include<vector>
#include<cstdio>
#define ll long long
#define llu long long unsigned
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
using namespace std;
int n;//总质量
int kind=0;//记方案数量
int arr[10];//记方案
int m[10000][10];//存入方案全部
void dfs(int total,int a)//a为枚举到了哪个调料,total为已选调料的总质量
{
if(a==10)//边界条件
{
if (total==n)
{
for(int j=0;j<10;j++)
m[kind][j]=arr[j];//存入
kind++;//计数量
}
return;
}
if(total>n) return;//剪枝,当总质量超过限制时返回上一层
for(int i=1;i<=3;i++)
{
arr[a]=i;
dfs(total+i,a+1);//入递归
}
}
int main()
{
IOS
cin>>n;
dfs(0,0);
cout<<kind<<endl;
for(int j=0;j<kind;j++)
{
for(int i=0;i<10;i++)
cout<<m[j][i]<<" ";
cout<<endl;
}
return 0;
}
2.组合:
例1:P1157:P1157 组合的输出 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
组合也就把排列中有相同数字的去重了,利用STL的next_permutation函数排列一个包含r个0,长度为n的bool型数组,需要输出的设为0,只输出bool数组中为0的下标即可。。。
利用STL解决:
#include<iostream>
#include<algorithm>
#include<string>
#include<queue>
using namespace std;
#define long long ll
#define unsigned long long ull
bool s[100010];
int main()
{
int n,r;
cin>>n>>r;
for(int i=r+1;i<=n;i++)
s[i]=1;//保留r个0,下标从1开始
do{
for(int i=1;i<=n;i++)
{
if(s[i]==0)
printf("%3d", i); //如果s[i]为0,则输出该数字,注意格式。
}
printf("\n"); // 换行
}while(next_permutation(s+1,s+n+1));
return 0;
}
感觉上面的写法很有技巧呢。。。来看看搜索。。
还是要考虑两个问题:
1.枚举到何位置?
我们和全排列一样可以定义一个位置数组存放解,当位置到达上限时停止。
2.位置上放啥数字?
数字要求不能重复,但同时还要求每种组合中保证出现的数字不能全一样。。。
于是我们可以用一个变量来记录选取的数字是哪个,一个一个选,把含有这个数字所有组合方案输出来,之后就不选了呗
这样我们的dfs:
int n,r;
int a[200];
void dfs(int step,int next)//next表示下一步选取的数字开始
边界与递归步骤同理,直接上代码:
#include<iostream>
#include<string>
#include<queue>
#define long long ll
#define unsigned long long ull
using namespace std;
int n,r;
int a[200];
void dfs(int step,int next)//next表示下一步选取的数字开始
{
if(step==r+1)
{
for(int i=1;i<=r;i++)
printf("%3d",a[i]); //输出当前的组合
printf("\n");
return;
}
for(int i=next;i<=n;i++) //从next开始遍历可选的数字
{
a[step]=i; //将数字i加入当前组合
dfs(step+1,i+1); //继续递归选择下一个数字,下一个数字从i+1开始
}
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>r;
dfs(1,1);
return 0;
}
例2:P1036 P1036 [NOIP2002 普及组] 选数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
关于暴力枚举就不多介绍了,不过社区中还真有人这样做(○´・д・)ノ,把蒟蒻吓哭了。。。
对比明显了吧◕‿◕。。。
也变向为组合问题的一种,各种方案选取的所有数字不能完全一样,就加了个判断素数。。。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<vector>
#define ll long long
using namespace std;
bool findprime(ll n)
{
ll i;
if(n==1||n==0)
return 0;
for(i=2;i*i<=n;i++)
if(n%i==0)
return 0;
return 1;
}//判断素数,算基础了不说了哒哒哒。。。
ll s[100];//存可以选的数
ll n,m,cn;//n为可以选的数的数量,m为要选几个数,cn为方案总数
void dfs(int x,int sum,int start)//x为选到第几个数,sum为选到数的和,从start遍历选数
{
if(x==m)//边界条件
{
if(findprime(sum))//是素数就入方案
cn++;
return; //返回
}
for(int i=start;i<n;i++)
dfs(x+1,sum+s[i],i+1);//从start开始入递归
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(ll i=0;i<n;i++)
cin>>s[i];
dfs(0,0,0);//入递归
cout<<cn;
return 0;
}
3.处理图:
例1:P3654:First Step (ファーストステップ) - 洛谷
典型图搜索问题,算最好理解的呢qwq,不过有坑。。。(宕机(灬ꈍ ꈍ灬))
思路就是判断每一个点向下和向右是否能满足条件(或者向下向左、向上向右、向上向左,一定不是向上向下或向左向右!!!),但是r=1时需要特判,因为r=1意味着向下和向右重复计算,需要除以2。(要不然只能得WA80辣!)。
还有就是lovelive! sunshine ! 但是BanG Dream!Is's MyGO!启动(*❦ω❦)!!!
上代码:
#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
#define llu long long unsigned
#define pq priority_queue
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
const int N=1e3+10;
int n,m,r;
int dx[2]={0,1},dy[2]={-1,0};//两个方向
char mp[1001][1001];
int ans;
void dfs(int x,int y,int cn,int sum)
{
if(sum>r)//符合条件记录,返回上一层
{
ans++;
return;
}
if(x<0||y<0||x>=n||y>=m||mp[x][y]!='.')//边界返回上一层
return;
dfs(x+dx[cn],y+dy[cn],cn,sum+1);
}
int main()
{
IOS
cin>>n>>m>>r;
for(int i=0;i<n;i++)
cin>>mp[i];
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if(mp[i][j]=='.')
for(int k=0;k<2;k++)
dfs(i,j,k,1);//两个方向进入dfs
if(r==1)//特判
cout<<ans/2;
else
cout<<ans;
return 0;
}
二,刷洛谷(又🤮)
赶紧给大家讲解(粘代码)
P1149 P1149 [NOIP2008 提高组] 火柴棒等式 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我滴火柴人。。。
#include <iostream>
#include <algorithm>
#include <cstdio>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
// 火柴棍数目对应的数字所需火柴棍数目
int n[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int N; // 目标数字所需的总火柴棍数目
int res; // 符合条件的方案数量
int arr[3], s[2050]; // arr数组用于存储当前选取的数字,s数组用于存储每个数字所需的火柴棍数目
void dfs(int pos, int sum)
{
if (sum > N) // 如果当前火柴棍数目已经超过目标数,则结束当前递归
return;
if (pos >= 3) // 当前已选取了3个数字
{
if (arr[0] + arr[1] == arr[2] && sum == N) // 满足条件:前两个数字之和等于第三个数字,并且火柴棍数目达到目标数
res++; // 方案数加一
return;
}
for (int i = 0; i <= 710; i++)
{
arr[pos] = i; // 尝试选取数字i
dfs(pos + 1, sum + s[i]); // 递归选择下一个数字,并更新火柴棍数目总和
arr[pos] = 0; // 回溯,取消选择数字i
}
}
int main()
{
IOS
cin >> N;
N -= 4; // 减去“four”的火柴棍数目
for (int i = 0; i <= 1000; i++)
{
int x = i;
if (x == 0)
s[i] = n[x];
else
{
while (x != 0)
{
s[i] += n[x % 10]; // 计算数字i所需的火柴棍数目
x /= 10;
}
}
}
dfs(0, 0); // 从第一个位置开始深度优先搜索
cout << res; // 输出符合条件的方案数量
return 0;
}
P2036 P2036 [COCI2008-2009 #2] PERKET - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
条件多了些而已。。。没啥区别。。
#include <cstdio>
#include <cmath>
#include <iostream>
using namespace std;
int n; // 物质数量
int acid[20], bitter[20]; // 酸度和苦味数组
int res = 1e9; // 记录最小差值
void dfs(int idx, int acidsum, int bittersum)
{
if (idx == n) // 遍历完所有物质
{
if (acidsum != 1 && bittersum != 0) // 排除初始值情况
{
res = min(res, abs(acidsum - bittersum)); // 更新最小差值
}
return;
}
// 选择当前元素
dfs(idx + 1, acidsum * acid[idx], bittersum + bitter[idx]);
// 不选择当前元素
dfs(idx + 1, acidsum, bittersum);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
scanf("%d %d", &acid[i], &bitter[i]); // 输入各物质的酸度和苦味值
}
dfs(0, 1, 0); // 从第一个元素开始递归,初始酸度为1,初始苦味为0
cout << res; // 输出最小差值
return 0;
}
P1088:P1088 [NOIP2004 普及组] 火星人 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
还是搜索全排列问题,这次是记录排列数量,本题还有中方法叫康托展开,感兴趣朋友们自查,本蒟蒻还不会呢。。。。
直接上dfs代码:
#include <iostream>
#include <cmath>
#include <string>
#include <vector>
#include <cstdio>
#define ll long long
#define llu long long unsigned
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define N 10010
using namespace std;
int n, m;
int arr[N]; // 记录方案
int mars[N]; // 记录火星人排列
bool st[N]; // 记录选择的数字
int res = 0; // 记录次数
bool stop = 0; // 停止
void dfs(int x) // DFS函数,x表示当前位置
{
if (stop)
return;
if (x > n)
{
res++;
if (res == m + 1)
{
stop = 1;
for (int i = 1; i <= n; i++)
cout << arr[i] << " ";
}
// return;
}
for (int i = 1; i <= n; i++)
{
if (!res)
i = mars[x];
if (!st[i])
{
st[i] = 1; // 标记数字i被选择
arr[x] = i; // 在第x个位置选择数字i
dfs(x + 1); // 递归到下一个位置
st[i] = 0; // 回溯,取消选择数字i
arr[x] = 0;
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> mars[i];
dfs(1); // 从第一个位置开始进行深度优先搜索
return 0;
}
P1618:P1618 三连击(升级版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
升级咯。。。。
利用next_permutation函数可以完美解决
#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
#define ll long long
#define llu long long unsigned
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define pq priority_queue
const int N=1e9+10;
int s[9]={1,2,3,4,5,6,7,8,9};
ll sum=0;
int main()
{
IOS
int a,b,c;
cin>>a>>b>>c;
do{
ll x=s[0]*100+s[1]*10+s[2];
ll y=s[3]*100+s[4]*10+s[5];
ll z=s[6]*100+s[7]*10+s[8];
if(x*b==y*a&&x*c==z*a&&y*c==z*b)
{
sum++;
cout<<x<<" "<<y<<" "<<z<<endl;
}
}while(next_permutation(s,s+9));
if(!sum)
cout<<"No!!!\n";
return 0;
}
P2241: P2241 统计方形(数据加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
正解是数学问题。。。画画图就明白了。。。。
#include <iostream>
#include<cstdio>
#include<cmath>
#define ll long long
using namespace std;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
ll n,m,sum1=0,sum2=0;
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
if(i==j)
sum1+=(n-i)*(m-j);//i==j就是正方形
else
sum2+=(n-i)*(m-j);//不是为矩形
}
cout<<sum1<<" "<<sum2;
return 0;
}
P3392:P3392 涂条纹 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
简单题直接枚举,搜索反而麻烦。。。
#include<iostream>
#include<cstdio>
using namespace std;
#define N 666666
int n,m,i,j,k,g;
char arr[55][55];
int ans=N;
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=n;i++)
{
for (j=1;j<=m;j++)
{
cin>>arr[i][j];
}
}
for (i=1;i<=n-2;i++)
for (j=i+1;j<=n-1;j++)
{
int sum=0;
for (k=1;k<=i;k++)
for (g=1;g<=m;g++)
if (arr[k][g]!='W')
sum++;
for (int k = i + 1; k <= j; k++)
for (int g = 1; g <= m; g++)
if (arr[k][g] != 'B')
sum++;
for (int k = j + 1; k <= n; k++)
for (int g = 1; g <= m; g++)
if (arr[k][g] != 'R')
sum++;
if (sum < ans)
ans = sum;
}
printf("%d\n", ans);
return 0;
}
P1217:
P1217 [USACO1.5] 回文质数 Prime Palindromes - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
枚举就能做。。。
#include <iostream>
#include <cmath>
#include <vector>
#define ll uint64_t
using namespace std;
bool isPrime(ll n, const vector<bool>& primes)
{
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
return primes[n];
}
bool isPalindrome(ll n)
{
ll num = n, rev = 0;
while (num > 0)
{
rev = rev * 10 + num % 10;
num /= 10;
}
return n == rev;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
ll n, m;
cin >> n >> m;
// 预处理质数表
vector<bool> primes(m + 1, true);
primes[0] = primes[1] = false;
for (ll i = 2; i * i <= m; ++i)
{
if (primes[i])
{
for (ll j = i * i; j <= m; j += i)
{
primes[j] = false;
}
}
}
if (n <= 2 && m >= 2) cout << "2\n";
if (n % 2 == 0) n++;
for (ll i = n; i <= m; i ++)
{
if (isPalindrome(i) && isPrime(i, primes))
{
cout << i << "\n";
}
}
return 0;
}
P2392:P2392 kkksc03考前临时抱佛脚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
不要认为是贪心了呢。。。。
#include<bits/stdc++.h>
using namespace std;
int Left, Right, minn, ans; // 左侧总和、右侧总和、当前最小差值、答案
int s[5]; // 每个集合的元素数量
int a[21][5]; // 存储每个元素的值
void dfs(int x, int y)
{
if(x > s[y])
{
minn = min(minn, max(Left, Right)); // 更新最小差值
return;
}
Left += a[x][y];
dfs(x + 1, y); // 选择当前元素
Left -= a[x][y];
Right += a[x][y];
dfs(x + 1, y); // 不选择当前元素
Right -= a[x][y]; // 回溯
}
int main()
{
cin >> s[1] >> s[2] >> s[3] >> s[4]; // 输入每个集合的元素数量
for(int i = 1; i <= 4; i++)
{
Left = Right = 0;
minn = 19260817; // 初始化最小差值为一个较大的数
for(int j = 1; j <= s[i]; j++)
cin >> a[j][i]; // 输入每个元素的值
dfs(1, i); // 开始搜索
ans += minn;
}
cout << ans; // 输出答案
return 0;
}
好了,第二篇就到这里了,
搜索折磨死我了哇哇哇哇。。。。
下篇再见呢!!!!!