搜索
1.深度优先搜索DFS
(时间复杂度十分玄学,加了剪枝后,即使最坏复杂度是指数级别的,但往往跑得很快)
1.1.搜索模型
方向数组:
const int dx[]={0,-1,0,1},dy[]={1,0,-1,0};//4个方向
for (int i=0;i<4;i++)
{
int a=x+dx[i],b=y+dy[i];
//abaabaaba
}
1.1.1.连通性模型(不常用)
一般连通性问题推荐使用BFS《搜索2.宽度优先搜索BFS》。
回溯问题:a.对于“内部搜索”,即在一张图上搜点,无需回溯,时间复杂度与点的个数成线性关系;b.对于“外部搜索”,即以一张图为一个状态搜索下一个状态,需要回溯。
const int dx[]={0,-1,0,1},dy[]={1,0,-1,0};
bool st[N][N];
bool dfs(int x,int y)
{
if (g[x][y]=='#') return false;
if (x<0 || x>=n || y<0 || y>=n) return false;
if (st[x][y]) return false;
if (x==xb && y==yb) return true;
st[x][y]=true;
for (int i=0;i<4;i++)
{
int a=x+dx[i],b=y+dy[i];
if (dfs(a,b)) return true;
}
return false;
}
1.2.搜索变形
1.2.1.迭代加深
适用条件:递归层数比较多、搜索空间比较大,但是答案在较浅层的问题。\(e.g.\)“如果10步以内搜不到结果就算无解”、可以估算答案≤10。
bool dfs(int k,int depth){
if(k==depth && check()) return true;
for()
{
ans[k]=;
if(dfs(k+1,depth)) return true; //此处不能写成return dfs(k+1,depth);否则得到false直接结束搜索
}
return false;
}
int main(){
int depth=1;
while(!dfs(1,depth)) depth++;
//注意这里是depth-1
printf("%d\n",depth-1);
for(int i=1;i<=depth-1;i++) printf("%d ",ans[i]);
return 0;
}
1.3.搜索优化
1.3.1.搜索顺序
搜索顺序的确定很灵活,但需要满足一个要求:不重不漏。
1.3.2.剪枝
必须先确定搜索完整性和搜索顺序。
- 优先搜索分枝少的节点
- 排除等效冗余:a.按组合数枚举(传递一个参数start。e.g.枚举到编号5,下次递归从6开始);b.若一个数据不满足条件,则另一些与之相同的数据可以直接跳过;c.(以木棒为例):如果某个木棒第一根木棍失败了则一定失败;如果某个木棒最后一根木棒失败了则一定失败。
- 可行性剪枝(不满足条件直接return)
- 最优性剪枝(适用于求最值,定义全局变量ans=INF(或-INF),当某个节点k>ans(或k<ans)直接return)
- 记忆化搜索(空间换时间)
1.3.3.双向DFS
适用条件:搜索的范围巨大,最终或中途目标状态确定,搜索路径可逆。
采用空间换时间的技巧,前一半打表,后一半搜索时利用前一半的结果。可将时间复杂度降为\(O(2^{\frac{N}{2}})\)。
- 例题:超大体积的01背包
const int N=1<<24;
int w,n,k,ans;
int g[50];
vector<int> f; //双向搜索的其中一个方向的打表
bool cmp(int a,int b)
{
return a>b;
}
//前一半
void dfs_pre(int u,int s)
{
if(u>k)
{
f.push_back(-s); //因为找小于等于x的最大的数,故加负号
return ;
}
if((LL)s+g[u]<=w) dfs_pre(u+1,s+g[u]); //选
dfs_pre(u+1,s); //不选
return ;
}
//后一半
void dfs_ne(int u,int s)
{
if(u>n)//前一半加后一半
{
int res=lower_bound(f.begin(),f.end(),-(w-s))-f.begin();
if(res==f.size()) ans=max(ans,s);
else if(s+(-f[res])<=w) ans=max(ans,s+(-f[res]));
return ;
}
if((LL)s+g[u]<=w) dfs_ne(u+1,s+g[u]);//选
dfs_ne(u+1,s);//不选
return ;
}
int main()
{
scanf("%d%d",&w,&n);
for(int i=1;i<=n;i++) scanf("%d",&g[i]);
sort(g+1,g+n+1,cmp);//优化搜索顺序,体积大的两个dfs的分支小
k=n/2;
dfs_pre(1,0);
sort(f.begin(),f.end());
f.erase(unique(f.begin(),f.end()),f.end());
dfs_ne(k+1,0);
printf("%d\n",ans);
return 0;
}
1.3.4.\(\text{IDA*}\)
适用条件:只要能设计出正确的估价函数均适用。
迭代加深+估价函数。
估价函数的估值必须不大于未来实际代价!
难点在于设计出优秀的估价函数。当估价函数始终为0,等价于普通的迭代加深。
若当前深度+未来估计步数>深度限制,回溯。
//估价函数
int f(){
//abaabaaba
return ;
}
//IDA*:dfs迭代加深+估价函数
bool IDA_star(int depth,int depth_max){
if(depth+f()>depth_max) return false;
if(check()) return true;
for()
{
if(IDA_star(depth+1,depth_max)) return true;
}
return false;
}
//dfs迭代加深
int depth=1;
while(!IDA_star(1,depth)) depth++;
2.宽度优先搜索BFS
- 在访问完所有第i层的节点后,才会开始访问第i+1层节点;
- 广搜队列中的元素关于层次满足“两段性”和“单调性”;
- 当BFS具有“单调性”时,BFS第一次出队搜到的点一定符合“最短”的性质;否则,队列为空时才能得到最优解。
2.1.DFS与BFS
- 数据结构:DFS 用的是 stack ,BFS 用的是 queue。
- 时间:均为O(n+m);空间:DFS 是 O(h) ,BFS 是 O(2^h) ( h代表深度)。
- BFS能不仅能告诉我们是否能到某点,还能告诉我们最短路径,但 DFS只能告诉我们一个点是否可达,不能告诉我们最短路径。但他们的边的权值均为 1 。
2.2.搜索模型
方向数组:
const int dx[]={0,-1,0,1},dy[]={1,0,-1,0};//4个方向
for (int i=0;i<4;i++)
{
int a=x+dx[i],b=y+dy[i];
//abaabaaba
}
2.2.1.泛洪模型
特征:在线性时间内求出在给定的图中每个点所在的连通块。
// const int gx[4]={1,-1,0,0},gy[4]={0,0,1,-1};//4连通
int n,m;
// int g[N][N]; //地图
bool vis[N][N];
void bfs(int sx,int sy)
{
queue<PII>q;
q.push({sx,sy});
vis[sx][sy]=true;
while(q.size())
{
PII t=q.front();
q.pop();
// 8连通:
for(int i=t.x-1;i<=t.x+1;i++)
for(int j=t.y-1;j<=t.y+1;j++)
{
if(i==t.x && j==t.y) continue;
if(i<1 || i>n || j<1 || j>m) continue;
if(vis[i][j]) continue;
// 其他判断条件
// 其他处理
q.push({i,j});
vis[i][j]=true;
}
/* 4连通:
for(int i=0;i<4;i++)
{
int a=t.x+gx[i],b=t.y+gy[i];
// 同理上面8联通的步骤
}
*/
}
return;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(!vis[i][j])
{
bfs(i,j);
}
2.2.2.最短路模型
相比于图论中求最短路,《2.2.2.1.》、《2.2.2.2.》与《2.2.2.4.》能在线性时间内求出最短路。
2.2.2.1.边权均为 “1” 的最短路——BFS
特征:由于BFS具有两段性和第一次搜到的点一定符合“最短”的性质,故能线性地求解边权均为 “1”(广义:边权都相同) 的一张图的最短路问题。\(e.g.\)走迷宫。
由于“两端性 ”,故第一次出队就是最优解。
-
例题:走迷宫
有一个处理路径输出的技巧:因为我们输出时的路径是反向搜的,因此如果我们一开始 BFS 的时候就反向搜,那么我们最终搜到的路径就是正的了,直接输出即可。
const int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
int n;
int g[N][N];
PII pre[N][N];
void bfs(int sx,int sy)
{
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
pre[i][j]={-1,-1};
queue <PII> q;
q.push({sx,sy});
pre[sx][sy]={0,0};
while (q.size())
{
PII t=q.front();
q.pop();
for (int i=0;i<4;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if (a<1 || a>n || b<1 || b>n)
continue;
if (g[a][b])
continue;
if (pre[a][b].x!=-1)
continue;
q.push({a,b});
pre[a][b]=t;
}
}
return;
}
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
scanf("%d",&g[i][j]);
bfs(n,n);
PII ed={1,1};
while (true)
{
printf("%d %d\n",ed.x-1,ed.y-1);
if (ed.x==n && ed.y==n)
break;
ed=pre[ed.x][ed.y];
}
}
2.2.2.2.0“1”边权最短路——双端队列广搜
特征:求**边权只有 0 或“ 1”(广义:边权只有2种:0和某一相同边权) **的图的最短路。
做法:定义deque<X> q;每次从队头取出一个元素,对其进行扩展。如果扩展的边权重为0,就把扩展到的点插入队头;如果权重为1,就把扩展到的点插入队尾。
这种做法的本质就是维护队列单调性和两段性。
由于按边权01排序,故第一次出队就是最优解。
- 例题:电路维修
#define x first
#define y second
int dx[4]={-1,-1,1,1},dy[4]={-1,1,1,-1};
int ix[4]={-1,-1,0,0},iy[4]={-1,0,0,-1};
char ele[]="\\/\\/";
int n,m,t,ans;
int dis[N][N];
char mapp[N][N];
bool vis[N][N];
int bfs(){
memset(vis,0,sizeof vis);
memset(dis,0x3f,sizeof dis);
deque<PII> q;
q.push_back({0,0});
dis[0][0]=0;
while(q.size()){
auto t=q.front();
q.pop_front();
if(vis[t.x][t.y]) continue;
if(t.x==n && t.y==m) return dis[n][m];
vis[t.x][t.y]=1;
for(int i=0;i<4;i++){
int a=t.x+dx[i],b=t.y+dy[i];
int j=t.x+ix[i],k=t.y+iy[i];
if (a>=0 && a<=n && b>=0 && b<=m){
int w=0;
if (mapp[j][k]!=ele[i]) w=1;
if (dis[a][b]>dis[t.x][t.y]+w){
dis[a][b]=dis[t.x][t.y]+w;
if(w) q.push_back({a,b});
else q.push_front({a,b});
}
}
}
}
if(dis[n][m]==0x3f3f3f3f) return -1;
return dis[n][m];
}
int main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%s",mapp[i]);
ans=bfs();
if(ans==-1) puts("NO SOLUTION");
else printf("%d\n",ans);
}
return 0;
}
2.2.2.2.1.边权较小——拆点/拆边转化为 01 最短路。
2.2.2.3.边权均为0或1或2的最短路——两个队列广搜
2.2.2.4.边权普遍的最短路
特征:边权普遍的图的最短路。
边权为正——优先队列广搜
采用优先队列进行广搜:堆优化的Dijkstra。
由于按边权排序,故第一次出队就是最优解。
边权任意
一般的队列多次“迭代”更新直至“收敛”:SPFA。
由于此时队列中的元素不具有“单调性”,故队列为空时才能保证最优解。
2.2.2.5.边权均为1求任意一个点到所有起点的最短路——多源BFS
特征:求边权均为1的图上任意一个点到所有起点的最短距离。
建立一个虚拟源点,从它向所有起点连一条边权为 0的边。(实际上大部分多源问题都可以这样转化为单源问题,且对于多个终点也是这么处理的)
实际上实现的时候我们可以不真正地建立一个虚拟源点,我们只需要把所有起点的距离初始化为 0 ,然后全部入队即可。(即Flood Fill的变形)
2.2.3.最小步数模型
特征:每个事件或状态之间的转移。将每个状态看作1个点,要求从起始状态转移到最终状态的最小步数和操作。\(e.g.\)魔板。
每一步的状态储存:哈希表:unordered_map。
-
例题:拯救大兵瑞恩——有钥匙的迷宫
BFS+状态压缩。
用状态压缩保存身上的钥匙情况。二进制下第i位是1表示有第i种类型的钥匙。
const int dx[]={1,-1,0,0};
const int dy[]={0,0,1,-1};
int n,m,p,k,s;
int g[N][N][N][N];//g[x1][y1][x2][y2]:从(x1,y1)到(x2,y2)所需要的钥匙类型
int key[N][N];//key[x][y]:(x,y)放的钥匙
int dis[N][N][1<<N];//dis[x][y][state]最小步数
struct Data
{
int x,y,state;
};
int bfs()
{
memset(dis,0x3f,sizeof dis);
queue<Data> q;
q.push({1,1,key[1][1]});
dis[1][1][key[1][1]]=0;
while(q.size())
{
auto t=q.front();
q.pop();
if(t.x==n && t.y==m) return dis[t.x][t.y][t.state];
for(int i=0;i<4;i++)
{
int x_ne=t.x+dx[i],y_ne=t.y+dy[i];
if(x_ne<=0 || x_ne>n || y_ne<=0 || y_ne>m) continue;
int &door=g[t.x][t.y][x_ne][y_ne];
if(door==0 || (door>=1 && !((t.state>>door)&1))) continue;
int state_ne=t.state|key[x_ne][y_ne];
if(dis[x_ne][y_ne][state_ne]>dis[t.x][t.y][t.state]+1)
{
dis[x_ne][y_ne][state_ne]=dis[t.x][t.y][t.state]+1;
q.push({x_ne,y_ne,state_ne});
}
}
}
return -1;
}
int main()
{
memset(g,-1,sizeof g);
scanf("%d%d%d",&n,&m,&p);
scanf("%d",&k);
while(k--)
{
int x1,y1,x2,y2,q;
scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&q);
g[x1][y1][x2][y2]=q;
g[x2][y2][x1][y1]=q;
}
scanf("%d",&s);
while(s--)
{
int x,y,q;
scanf("%d%d%d",&x,&y,&q);
key[x][y]|=1<<q;
}
printf("%d\n",bfs());
return 0;
}
2.2.4.双重广搜模型
特征:一个状态包含2个变量。\(e.g.\)人推箱子。
算法本身是一个BFS,只不过每一步拓展的计算函数expand内恰好又需要使用BFS。
2.3.搜索优化
2.3.1.双向广搜
适用条件:搜索的范围巨大,最终目标状态确定,搜索路径可逆。可将时间复杂度降为\(O(2^{\frac{N}{2}})\)。
方法一
- 先搜索一半,并存储结果。
int n,base,s1,s2;
LL tot;
bool flag;
// vector<LL> pre; //当要求总和<=tot时
int hidx;
unordered_map<LL,int> h;
vector<int> h_state[N];
void dfs_pre(int id,int state)
{
if(id==base+1)
{
int res=calc();
if(!h.count(res))
{
h[res]=++hidx;
// pre.push_back(res); //当要求总和<=tot时
}
h_state[h[res]].push_back(state);
return ;
}
dfs_pre(id+1,state);
if(calc()<=tot) dfs_pre(id+1,state|(1<<(id-1)));
return ;
}
void dfs_ne(int id,int state)
{
if(flag) return ;
if(id==n+1)
{
LL res=tot-calc();
//当要求总和=tot时
if(h.count(res))
{
for(int it : h_state[h[res]])
if(/*it和state的限定条件,例如全都不为0:id!=0 && state!=0*/)
{
s1=it,s2=state;
flag=true;
return ;
}
}
/*当要求总和<=tot时
res=lower_bound(pre.begin(),pre.end(),res)-f.begin();
abaabaaba
*/
return ;
}
dfs_ne(id+1,state);
if(calc()<=tot) dfs_ne(id+1,state|(1<<(id-base-1)));
return ;
}
scanf("%d%d",&n,&tot);
base=n>>1;
dfs_pre(1,0);
dfs_ne(base+1,0);
if(!flag)
{
puts("IMPOSSIBLE");
return ;
}
for(int i=1;i<=base;i++) if((s1>>(i-1))&1) //abaabaaba
for(int i=base+1;i<=n;i++) if((s2>>(i-base-1))&1) //abaabaaba
方法二
- 直接从起点和终点分别扩展一步;
- 小小的优化——每次选择两个队列里面元素较少的一端扩展。
- 扩展的一步是扩展完整的一层而不是一个点,对应到代码中就是每次扩展要把与队头的dist值相等的点全部扩展一遍。
- 判定无解:如果两边一旦有一边扩展完了,且还未相遇,那么起点和终点一定不连通,无解。
int expand(queue <X> &qa, int da[ ]){
int d=da[qa.front()];
while (qa.size() && da[qa.front()]==d){
auto t=qa.front();
qa.pop();
//xxxxx
}
return /*xxx*/;
}
int bfs(){
queue <X> qa,qb;
qa.push(/*xxx*/);
qb.push(/*xxx*/);
da[A]=db[B]=0;
int t,step;
while (qa.size() && qb.size()){
if(qa.size()<=qb.size()) t=expand(qa,da);
else t=expand(qb,db);
if(t<=/*xxx*/) return t;
if (t>/*xxx*/) return INF;
}
return INF;
}
2.3.2.\(\text{A*}\)
适用条件:只要能设计出正确的估价函数均适用。
优先队列广搜+估价函数。
估价函数的估值必须不大于未来实际代价!
难点在于设计出优秀的估价函数。当估价函数始终为0,等价于普通队列的BFS。
其中优先队列存的是:从起点到当前点的实际距离+从当前点到终点的估计距离。未来的搜索中,优先取出“当前代价+未来估价”最小的状态进行拓展。
由于队列具有“单调性”,故第一次出队就是最优解。
while(!q.empty()){
t ← 优先队列的队头
if(终点第一次出队时) return ;
for(t的所有邻边)
将邻边入队
}
2.3.2.1.\(\text{A*}\)求第k短路
第一次出队是最短路,第k次出队是第k短路。
第k短路:e.g.若有边\((1,2,1),(1,2,1),(1,2,2)\),则1→2的第1,2,3短路分别为1,1,2。
int h[N],rh[N],e[M],w[M],ne[M],idx;
int d[N],cnt[N];
int n,m,s,t,k;
bool vis[N];
void add(int hh[],int a,int b,int c){
e[++idx]=b;
w[idx]=c;
ne[idx]=hh[a];
hh[a]=idx;
return ;
}
//反向边求估价函数
void dij(){
priority_queue<PII,vector<PII>,greater<PII> > q;
memset(d,0x3f,sizeof d);
d[t]=0;
q.push({d[t],t});
while(q.size())
{
auto tt=q.top();
q.pop();
int u=tt.second;
if(vis[u]) continue;
vis[u]=true;
for(int i=rh[u];i!=0;i=ne[i])
{
int v=e[i];
if(d[v]>d[u]+w[i])
{
d[v]=d[u]+w[i];
q.push({d[v],v});
}
}
}
return ;
}
int A_star(){
priority_queue<PIII,vector<PIII>,greater<PIII> > q;
q.push({d[s],{0,s}});
while(q.size())
{
auto tt=q.top();
q.pop();
int u=tt.second.second,dis=tt.second.first;
cnt[u]++;
if(cnt[t]==k) return dis; //第一次出队是最短路,第k次出队是第k短路
for(int i=h[u];i!=0;i=ne[i])
{
int v=e[i];
if(cnt[v]<k) q.push({dis+w[i]+d[v],{dis+w[i],v}}); //注意这里的判定条件
}
}
return -1;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,wor;
cin>>u>>v>>wor;
add(h,u,v,wor);
add(rh,v,u,wor);
}
cin>>s>>t>>k;
if(s==t) k++;
dij();
cout<<A_star()<<endl;
return 0;
}
3.DLX
《数据结构·字符串和图.DLX》
4.三分
适用条件:图像是单峰函数(凸函数,非最值处不能有平台)时。
- 如果
lmid和rmid在最大(小)值的同一侧:由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。 - 如果在两侧:由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。
实数域上的代码只需把下面的int改成double,1改成\(eps\)即可。
下面以求下凸函数最小值为例,若要求上凸函数最大值改为lres>rres即可。
//整数域
while(l<r)
{
int mid=(l+r)/2;
int lres=calc(mid),rres=calc(mid+1);
if(lres<rres) r=mid;//下凸函数最小值。若要求上凸函数最大值改为lres>rres即可。
else l=mid+1;
}
return l;
/*实数域
while(r-l>EPS)
{
double mid=(l+r)/2;
double lres=calc(mid-EPS),rres=calc(mid+EPS);
if(dcmp(lres,rres)<0) r=mid;//下凸函数最小值。若要求上凸函数最大值改为dcmp(lres,rres)>0即可。
else l=mid;
}
return l;
*/
三分求max(min(f(i),g(i))):也可二分套二分求解。
5.模拟退火
前提:函数具有连续性
5.1.调参数
注意是否会除0或模0!
注意开double!
\(1.\)温度(步长,有的题目随机一个新点时也不一定用到步长)
初始温度
\(T_0\)
一般取数据范围的极限至一半
终止温度
\(T_E\)
一般比精度再小3个数量级
衰减系数
T
取\((0,1)\),越靠近1,成功率越大,时间复杂度越大$e.g.$0.99
\(2.\)跳跃概率(以找最低峰为例)
随机选择一个点,\(\Delta E=f(新点)-f(当前点)\)
情况一:若\(\Delta E\)<0,则跳到新点;
情况二:若\(\Delta E\)>0则以一定的跳跃概率\(e^{-\dfrac{\Delta E}{T}}\)跳跃到新点。(防止局限于局部最优解)
一般用卡时的方法(注意看时限是1s还是0.8s)while((double)clock()/CLOCKS_PER_SEC</0.8~0.85/) simulate_anneal();循环多次模拟退火,这样找到错误的局部最优解的概率越低。
如果是多组测试数据的话,就不可以用卡时的方法,否则只会执行一组数据。一般循环100~1000次。
最低峰:if(exp(-delta/T)>drand(0,1)) now=neww;
最高峰:if(exp(delta/T)>drand(0,1)) now=neww;
对照大样例调参。
5.2.关于温度与随机新点
温度的作用:随机新点、判断跳跃概率。这里我们来谈随机新点:
- 当温度表示步长时,即几何问题或\(f(x)\)问题,随机一个新点一定要涉及步长和当前点,并且还要注意边界问题:
double neww=drand(max(1.0*L,cur-t),min(1.0*R,cur+t)); - 当温度与不能表示步长时,温度与随机新点无关,即交换问题,此时随机新点与当前点不能有太大差别,一般只能用
swap()操作,用random_shuffle()是不可取的:
int x=calc();//交换前
int a=rand()%n,b=rand()%n;//随机交换两项
if(vis[a]==vis[b]) continue;//此处不可以写成while,否则当vis数量接近于n时会超时查找vis不同的两个点
swap(q[a],q[b]);//有时可以是swap(vis[a],vis[b]);
int y=calc();//交换后
double delta=y-x;
5.3.注意事项
- 随机一个初始点时,若贪心得到有明显的更优的位置,就不用随机数,直接令初始点为那个更优的位置。
- 1次模拟退火结束后可以在求到的最优解周围取一圈最值。
5.4.具体步骤
double drand(double l,double r)
{
return (double)rand()/RAND_MAX*(r-l)+l;
}
double calc(double x)
{
//abaabaaba...
//还可以在calc里面结合贪心或DP
ans=min(ans,res);
return res;
}
//几何问题
void simulate_anneal()
{
double cur=drand(0,t0);//随机一个初始点
for(int t=t0;t>te;t*=T)
{
double new_p=drand(cur-t,cur+t);//在当前点周围随机一个点
double delta=calc(new_p)-clac(cur);
if(exp(-delta/t)>drand(0,1)) cur=new_p;
}
return ;
}
/*有限集中的最优解问题之一:交换问题
void simulate_anneal()
{
for(double t=1e4;t>1e-4;t*=0.99)
{
int x=calc();//交换前
int a=rand()%n,b=rand()%n;//随机交换两项
swap(q[a],q[b]);
int y=calc();//交换后
double delta=y-x;
if(exp(delta/t)<(double)rand/RAND_MAX) swap(q[a],q[b]);//如果不行就交换回来
}
return ;
}
*/
/*随机打乱:
mt19937 rnd(time(NULL));
shuffle(q+1,q+n+1,rnd);
*/
while((double)clock()/CLOCKS_PER_SEC</*0.8~0.85*/) simulate_anneal();
printf("%lf\n",ans);
对于有限集中的最优解,除此之外,我们还可以用贪心或DP结合模拟退火:先用模拟退火将集合缩小一部分,再在\(calc()\)里面用贪心或DP将剩余集合缩小到最优解。
5.5.应用
-
几何问题
找费马点
typedef pair<double,double> PDD;
int n;
PDD q[N];
double ans=1e8;
double drand(double l,double r)
{
return (double)rand()/RAND_MAX*(r-l)+l;
}
double get(PDD a,PDD b)
{
double dx=a.x-b.x,dy=a.y-b.y;
return sqrt(dx*dx+dy*dy);
}
double calc(PDD p)
{
double res=0;
for(int i=1;i<=n;i++) res+=get(q[i],p);
ans=min(ans,res);
return res;
}
void simulate_anneal()
{
PDD cur={drand(0,10000),drand(0,10000)};
for(double t=1e4;t>1e-3;t*=0.99)
{
PDD new_p={drand(cur.x-t,cur.x+t),drand(cur.y-t,cur.y+t)};
double dt=calc(new_p)-calc(cur);
if(exp(-dt/t)>drand(0,1)) cur=new_p;
}
return ;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lf%lf",&q[i].x,&q[i].y);
while((double)clock()/CLOCKS_PER_SEC<0.8) simulate_anneal();
printf("%.0lf\n",ans);
return 0;
}
-
有限集中的最优解
交换问题
typedef pair<int,int> PII;
const int N=55;
int n,m,ans;
PII q[N];
int calc()
{
int res=0;
for(int i=1;i<=m;i++)
{
res+=q[i].x+q[i].y;
if(i<=n)
{
if(q[i].x==10) res+=q[i+1].x+q[i+1].y;
else if(q[i].x+q[i].y==10) res+=q[i+1].x;
}
}
ans=max(ans,res);
return res;
}
void sa()
{
for(double t=1e4;t>1e-4;t*=0.99)
{
int x=calc(); //交换前
int a=rand()%(m+1)+1,b=rand()%(m+1)+1; //注意是左闭右开
swap(q[a],q[b]);
if(n+(q[n].x==10)==m)
{
int y=calc(); //交换后
double delta=y-x;
if(exp(delta/*求最大值,去负号*//t)</*不行就交换回来*/(double)rand()/RAND_MAX) swap(q[a],q[b]);
}
else swap(q[a],q[b]);
}
return ;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&q[i].x,&q[i].y);
if(q[n].x==10)
{
m=n+1;
scanf("%d%d",&q[m].x,&q[m].y);
}
else m=n;
while((double)clock()/CLOCKS_PER_SEC<0.8) sa();
printf("%d\n",ans);
return 0;
}
-
结合贪心或DP
贪心:均分数据
int n,m;
int w[N],s[M];
double ans=1e8;
double calc()
{
double res=0,avg=0;
memset(s,0,sizeof s);
for(int i=0;i<n;i++)
{
int k=1;
for(int j=0;j<m;j++) if(s[j]<s[k]) k=j;
s[k]+=w[i];
}
for(int i=0;i<m;i++) avg+=(double)s[i]/m;
for(int i=0;i<m;i++) res+=(s[i]-avg)*(s[i]-avg);
res=sqrt(res/m);
ans=min(ans,res);
return res;
}
void sa()
{
random_shuffle(w,w+n);
for(double t=1e6;t>1e-6;t*=0.95)
{
double x=calc();
int a=rand()%n,b=rand()%n;
swap(w[a],w[b]);
double y=calc();
double delta=y-x;
if(exp(-delta/t)<(double)rand()/RAND_MAX) swap(w[a],w[b]);
}
return ;
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++) cin>>w[i];
while((double)clock()/CLOCKS_PER_SEC<0.8) sa();
printf("%.2lf\n",ans);
return 0;
}
6.爬山法
适用条件:图像是单峰函数(凸函数,可以有平台)时,且不知道如何三分(毒瘤数学或毒瘤几何)。
-
先初始化当前决策:我们可以直观地认为各坐标的平均值比较优,将其作为初始决策。
-
开始爬山:计算出当前决策与球上的点的欧氏距离,并决定如何进行调整。
-
爬山进行一定次数后退出,输出解。
- 例题:球形空间产生器
//i:第i个点;j:第j维
int n;
double a[N][N];
double ans[N],dis[N],delta[N]; //ans[1][N]:球心;dis[i]:第i个点到球心的距离;delta[N]:第i个维度的偏移量
void calc()
{
double avg=0;
memset(dis,0,sizeof dis);
memset(delta,0,sizeof delta);
for(int i=1;i<=n+1;i++)
{
//本来求点i到球心的距离应该写成dis[i]+=sqrt(()*());但是因为精度问题在下面统一sqrt
for(int j=1;j<=n;j++) dis[i]+=(a[i][j]-ans[j])*(a[i][j]-ans[j]);
dis[i]=sqrt(dis[i]);
avg+=dis[i];
}
avg/=n+1;
for(int i=1;i<=n+1;i++)
for(int j=1;j<=n;j++)
delta[j]+=(dis[i]-avg/*大小*/)*(a[i][j]-ans[j]/*方向*/)/avg;
return ;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n+1;i++)
for(int j=1;j<=n;j++)
{
scanf("%lf",&a[i][j]);
ans[j]+=a[i][j];
}
for(int j=1;j<=n;j++) ans[j]/=n+1;//初始化当前决策
for(double t=1e4;t>1e-6;t*=0.99995)
{
calc();
for(int j=1;j<=n;j++) ans[j]+=delta[j]*t;
}
for(int j=1;j<=n;j++) printf("%.3lf ",ans[j]);
return 0;
}

浙公网安备 33010602011771号