搜索

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.剪枝

必须先确定搜索完整性和搜索顺序。

  1. 优先搜索分枝少的节点
  2. 排除等效冗余:a.按组合数枚举(传递一个参数start。e.g.枚举到编号5,下次递归从6开始);b.若一个数据不满足条件,则另一些与之相同的数据可以直接跳过;c.(以木棒为例):如果某个木棒第一根木棍失败了则一定失败;如果某个木棒最后一根木棒失败了则一定失败。
  3. 可行性剪枝(不满足条件直接return)
  4. 最优性剪枝(适用于求最值,定义全局变量ans=INF(或-INF),当某个节点k>ans(或k<ans)直接return)
  5. 记忆化搜索(空间换时间)

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

  1. 在访问完所有第i层的节点后,才会开始访问第i+1层节点;
  2. 广搜队列中的元素关于层次满足“两段性”和“单调性”;
  3. 当BFS具有“单调性”时,BFS第一次出队搜到的点一定符合“最短”的性质;否则,队列为空时才能得到最优解。

2.1.DFS与BFS

  1. 数据结构:DFS 用的是 stack ,BFS 用的是 queue。
  2. 时间:均为O(n+m);空间:DFS 是 O(h) ,BFS 是 O(2^h) ( h代表深度)。
  3. 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}})\)

方法一

  1. 先搜索一半,并存储结果。
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

方法二

  1. 直接从起点和终点分别扩展一步;
  2. 小小的优化——每次选择两个队列里面元素较少的一端扩展。
  3. 扩展的一步是扩展完整的一层而不是一个点,对应到代码中就是每次扩展要把与队头的dist值相等的点全部扩展一遍。
  4. 判定无解:如果两边一旦有一边扩展完了,且还未相遇,那么起点和终点一定不连通,无解。
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.三分

适用条件:图像是单峰函数(凸函数,非最值处不能有平台)时。

  • 如果 lmidrmid 在最大(小)值的同一侧:由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。
  • 如果在两侧:由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。

实数域上的代码只需把下面的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.关于温度与随机新点

温度的作用:随机新点、判断跳跃概率。这里我们来谈随机新点:

  1. 当温度表示步长时,即几何问题或\(f(x)\)问题,随机一个新点一定要涉及步长和当前点,并且还要注意边界问题:double neww=drand(max(1.0*L,cur-t),min(1.0*R,cur+t));
  2. 当温度与不能表示步长时,温度与随机新点无关,即交换问题,此时随机新点与当前点不能有太大差别,一般只能用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.爬山法

适用条件:图像是单峰函数(凸函数,可以有平台)时,且不知道如何三分(毒瘤数学或毒瘤几何)。

  1. 先初始化当前决策:我们可以直观地认为各坐标的平均值比较优,将其作为初始决策。

  2. 开始爬山:计算出当前决策与球上的点的欧氏距离,并决定如何进行调整。

  3. 爬山进行一定次数后退出,输出解。

  • 例题:球形空间产生器
//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;
}
posted @ 2025-10-14 00:52  Brilliance_Z  阅读(12)  评论(0)    收藏  举报