从零开始学回溯算法

本文在写作过程中参考了大量资料,不能一一列举,还请见谅。

回溯算法的定义:回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。

解题的一般步骤是:

1.定义一个解空间,它包含问题的解;

2.利用适于搜索的方法组织解空间;

3.利用深度优先法搜索解空间;

4.利用限界函数避免移动到不可能产生解的子空间。

问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。

话不多说,我们来看几个具体的例子慢慢理解它:

1.八皇后问题

该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 下面的解法参考了《算法竞赛入门经典》。如果我们逐行放置皇后则肯定没有任意两个皇后位于同一行,只需要判断列和对角线即可。使用一个二维数组vis[3][],其中vis[0][i]表示列,vis[1][i]和vis[2][i]表示对角线。因为(x,y)的y-x值标识了主对角线,x+y值标识了副对角线。由于y-x可能为负,所以存取时要加上n。

#include<cstring>
#include<iostream>
using namespace std;
int vis[3][15],tot;  

void search(int cur)
{
    int i,j;
    if(cur==8) tot++;
    else
    {
    	for(i=0;i<8;i++)
        {
            if(!vis[0][i]&&!vis[1][cur-i+8]&&!vis[2][cur+i])
            {
                vis[0][i]=1;
                vis[1][cur-i+8]=1;
                vis[2][cur+i]=1;
                search(cur+1);
                //改回辅助的全局变量
                vis[0][i]=0;
                vis[1][cur-i+8]=0;
                vis[2][cur+i]=0;
            }
        }
    }
}  

int main()
{
    search(0);
    cout<<tot<<endl;
}

2.图的着色问题

给定无向连通图G=(V,E)和m种不同的颜色,用这些颜色为图G的各顶点着色,每个顶点着一种颜色。如果一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称m为该图的色数。地图着色问题可转换为图的着色问题:以地图中的区域作为图中顶点,2个区域如果邻接,则这2个区域对应的顶点间有一条边,即边表示了区域间的邻接关系。著名的四色定理就是指每个平面地图都可以只用四种颜色来染色,而且没有两个邻接的区域颜色相同。

给定图和颜色的数目求出着色方法的数目,可以使用回溯法。

#define N 100
#include<iostream>
using namespace std;
int v,e,c,graph[N][N],color[N];
//顶点数,边数,颜色数
int sum;

bool ok(int k)
{
	for(int j=1;j<=v;j++)
	{
		if(graph[k][j]&&(color[j]==color[k])) return false;
	}
	return true;
}

void backtrack(int t)
{
	if(t>v) sum++;
 	else
 	{
    	for(int i=1;i<=c;i++)
		{
			color[t]=i;
   			if(ok(t)) backtrack(t+1);
   			//改回辅助的全局变量
   			color[t]=0;
		}
 	}
}

int main()
{
    int i,j;
    cin>>v>>e>>c;
	for(i=1;i<=v;i++)
	{
		for(j=1;j<=v;j++)
		{
			graph[i][j]=0;
		}
	}
	for(int k=1;k<=e;k++)
	{
		cin>>i>>j;
		graph[i][j]=1;
		graph[j][i]=1;
	}
	for(i=0;i<=v;i++) color[i]=0;
 	backtrack(1);
  	cout<<sum<<endl;
}

3.装载问题

有一批共n个集装箱要装上2艘载重量分别为c1和c2的船,其中集装箱i的重量为wi,且。装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘船。如果有,找出一种装载方案。例如当n=3,c1=c2=50且w=[10,40,40]时,则可以将集装箱1和2装到第一艘轮船上,而将集装箱3装到第二艘轮船上;如果w=[20,40,40],则无法将这3个集装箱都装上轮船。容易证明,如果一个给定装载问题有解,则首先将第一艘船尽可能装满再将剩余的集装箱装上第二艘船可得到最优装载方案。将第一艘船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近c1。用回溯法解装载问题,
 时间复杂度O(2^n),在某些情况下优于动态规划算法。剪枝方案是如果当前已经选择的全部物品载重量cw+剩余集装箱的重量r<=当前已知的最优载重量bestw,则删去该分支。

#include<iostream>
using namespace std;
int n;//集装箱数
int w[40];//集装箱重量
int c1,c2;//两艘船的载重量
int ans;//当前载重量
int bestans;//当前最优载重量
int r;//剩余集装箱重量  

void backtrack(int i)
{
    if(i>n)
    {
		if(ans>bestans) bestans=ans;
		return;
    }
    r-=w[i];
    if(ans+w[i]<=c1)
    {
      ans+=w[i];
      backtrack(i+1);
      //改回辅助的全局变量
      ans-=w[i];
    }
    if(ans+r>bestans) backtrack(i+1);
    //改回辅助的全局变量
    r+=w[i];
}    

int maxloading()
{
	ans=0;
	bestans=0;
    backtrack(1);
    return bestans;
}

int main()
{
	cin>>n>>c1>>c2;
 	int i=1;
 	int sum=0;
 	//集装箱总重量
 	while(i<=n)
	{
		cin>>w[i];
		r+=w[i];
		sum+=w[i];
 		i++;
 	}
	maxloading();
	if(bestans>0&&((sum-bestans)<=c2)) cout<<bestans<<endl;
 	else if(sum<=c2) cout<<bestans<<endl;
  	else cout<<"No"<<endl;
}

4.旅行商问题

旅行商问题即TSP问题(Traveling Salesman Problem)。假设有一个旅行商人要拜访N个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。TSP问题是一个NPC问题。w(vi,vj)表示城市i与j间的直接距离,w(vi,vj)=∞表示城市i与j间无直接路径。将图中n个顶点编号为1,2,…,n,以顶点1为起点,旅行回路描述为1,x1,x2, ..,xn,1,其中 x1,x2, ..,xn为顶点2,3,4,…,n的1个排列。因此解空间大小为(n-1)!。

有两个剪枝约束:

1.如果当前正在考虑的顶点j与当前路径中的末端结点i没有边相连,即w[i, j]=∞, 则不必搜索j所在分支。

例如,当前已有的部分路径为<1,2, ?, ?>, ,路径末端结点为2,根据路径组成规则,下一步可考虑将顶点3、4加入到部分路径中。但是,顶点2与4间无边,w(2,4)=∞, 因此可以不必考虑顶点4所在分支。

2.令到第i层结点为止,构造的部分解路径为<1,x[2],x[3],…,x[i-1], x[i],?,?,?>,该路径的权值总和为

假设已经知道直到第i-1层的部分解<1, x[2],x[3],…,x[i-1], ?, ? ,?>,从第i-1层结点选择顶点x[i],并向该分支往下搜索的界限函数为B(i)=cw(i-1)+w(x[i-1],x[i]),如果B(i)≥bestw则停止搜索x[i]分支及其下面的层 ,其中bestw代表到目前为止在前面的搜索中,从其它已经搜索过的路径中找到的最佳完整回路的总长度。

该问题最优解为<1,3,2,4,1>和<1,4,2,3,1>,对应的bestw=25。假设搜索另一分支<1,3,4,?>,当前路径对应的结点为I,长度=6+20=26>bestw=25,则结点I之下的路径被舍弃。

#include<cstring>
#include<iostream>
#define INF 10000
using namespace std;
int n,m;//n个点m条边
int bestans=INF,ans=0;//最优解和当前解
int bestroad[100],road[100];//最佳路径和当前路径
int graph[100][100];//图的矩阵
bool vis[100];//访问标记 

void backtrack(int i)
{
	if(i>n)
 	{
		if(graph[road[n]][1]!=INF&&(ans+graph[road[n]][1])<bestans)
		{
			bestans=ans+graph[road[n]][1];
			for(int j=1;j<=n;j++) bestroad[j]=road[j];
		}
	}
	else
	{
		for(int j=1;j<=n;j++)
		{
 			if(graph[road[i-1]][j]!=INF&&ans+graph[road[i-1]][j]<bestans&&!vis[j])
    		{
				road[i]=j;
				ans+=graph[road[i-1]][j];
				vis[j]=1;
				backtrack(i+1);
				//改回辅助的全局变量
				ans-=graph[road[i-1]][j];
				vis[j]=0;
      		}
		}
	}
}

int main()
{
	memset(graph,INF,sizeof(graph));
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int a,b;
		cin>>a>>b;
		cin>>graph[a][b];
		graph[b][a]=graph[a][b];
	}
	vis[1]=1;
	road[1]=1;
	//假设是从1开始
	backtrack(2);
	cout<<bestans<<endl;
	for(int i=1;i<=n;i++) cout<<bestroad[i]<<" ";
	cout<<1<<endl;
}

5.批处理作业调度问题

给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理,然后由机器2处理。作业Ji需(1≤i≤n)要机器j(1≤j≤2)的处理时间为tji。对于一个确定的作业调度,设Fji是作业i在机器j上完成处理的时间。所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和:。要求对于给定的n个作业,制定最佳作业调度方案,使其完成时间和达到最小。

tji 机器1 机器2
作业1 2 1
作业2 3 1
作业3 2 3

例如,对于这张表格所示的情况,3个作业有3!=6种可能调度方案,很显然最坏复杂度即为O(n!)。如果按照2,3,1的顺序,则作业2的完成时间为4,作业3的完成时间为8,作业1的完成时间为9,完成时间和为21。最优的作业调度顺序为最佳调度方案是1,3,2,其完成时间和为18。

#define MAX 200
#include<iostream>
using namespace std;
int* x1;//作业Ji在机器1上的工作时间
int* x2;//作业Ji在机器2上的工作时间
int number=0;//作业的数目
int* xorder;//作业顺序
int* bestorder;//最优的作业顺序
int bestvalue=MAX;//最优的时间
int xvalue=0;//当前完成用的时间
int f1=0;//机器1完成的时间
int* f2;//机器2完成的时间

void backtrack(int k)
{
	if(k>number)
	{
		for(int i=1;i<=number;i++) bestorder[i]=xorder[i];
  		bestvalue=xvalue;
	}
	else
	{
		for(int i=k;i<=number;i++)
  		{
           f1+=x1[xorder[i]];
           f2[k]=(f2[k-1]>f1?f2[k-1]:f1)+x2[xorder[i]];
           xvalue+=f2[k];
           swap(xorder[i],xorder[k]);
           if(xvalue<bestvalue) backtrack(k+1);
           swap(xorder[i],xorder[k]);
           xvalue-=f2[k];
           f1-=x1[xorder[i]];
		}
	}
}

int main()
{
	cout<<"请输入作业数目:";
 	cin>>number;
	x1=new int[number+1];
 	x2=new int[number+1];
  	xorder=new int[number+1];
   	bestorder=new int[number+1];
   	f2=new int[number+1];
   	x1[0]=0;
   	x2[0]=0;
   	xorder[0]=0;
   	bestorder[0]=0;
    f2[0]=0;
    cout<<"请输入每个作业在机器1上所用的时间:"<<endl;
    int i;
    for(i=1;i<=number;i++)
    {
		cout<<"第"<<i<<"个作业=";
		cin>>x1[i];
  	}
	cout<<"请输入每个作业在机器2上所用的时间:"<<endl;
 	for(i=1;i<=number;i++)
  	{
   		cout<<"第"<<i<<"个作业=";
     	cin>>x2[i];
  	}
   	for(i=1;i<=number;i++) xorder[i]=i;
    backtrack(1);
    cout<<"最节省的时间为:"<<bestvalue<<endl;
    cout<<"对应的方案为:";
    for(i=1;i<=number;i++) cout<<bestorder[i]<<"  ";
    cout<<endl;
}

6.再再论背包问题

从零开始学动态规划从零开始学贪心算法中我们已经讨论过了背包问题,这里我们再次用回溯法求解经典的零一背包问题。

#include<iostream>
using namespace std;
int n,c,bestp;//物品个数,背包容量,最大价值
int p[10000],w[10000],x[10000],bestx[10000];//物品的价值,物品的重量,物品的选中情况

void backtrack(int i,int cp,int cw)
{
    if(i>n)
    {
        if(cp>bestp)
        {
            bestp=cp;
            for(i=1;i<=n;i++) bestx[i]=x[i];
        }
    }
    else
	{
        for(int j=0;j<=1;j++)
        {
            x[i]=j;
            if(cw+x[i]*w[i]<=c)
            {
                cw+=w[i]*x[i];
                cp+=p[i]*x[i];
                backtrack(i+1,cp,cw);
                cw-=w[i]*x[i];
                cp-=p[i]*x[i];
            }
        }
	}
}

int main()
{
    bestp=0;
    cin>>c>>n;
    for(int i=1;i<=n;i++) cin>>w[i];
    for(int i=1;i<=n;i++) cin>>p[i];
    backtrack(1,0,0);
    cout<<bestp<<endl;
}

7.最大团问题

给定无向图G=(V, E),U是V的子集。如果对任意u,v属于U有(u,v)属于E,则称U是G的完全子图。G的完全子图U是G的当且仅当U不包含在G的更大的完全子图中。G的最大团是指G中所含顶点数最多的团。如果对任意u,v属于U有(u, v)不属于E,则称U是G的空子图。G的空子图U是G的独立集当且仅当U不包含在G的更大的空子图中。G的最大独立集是G中所含顶点数最多的独立集。G的补图G‘=(V‘,
E‘)定义为V‘=V且(u, v)属于E‘当且仅当(u, v)不属于E。

如图所示,给定无向图G={V, E},其中V={1,2,3,4,5},E={(1,2),(1,4),(1,5),(2,3),(2,5),(3,5),(4,5)}。根据最大团定义,子集{1,2}是图G的一个大小为2的完全子图,但不是一个团,因为它包含于G的更大的完全子图{1,2,5}之中。{1,2,5}是G的一个最大团。{1,4,5}和{2,3,5}也是G的最大团。右侧图是无向图G的补图G‘。根据最大独立集定义,{2,4}是G的一个空子图,同时也是G的一个最大独立集。虽然{1,2}也是G‘的空子图,但它不是G‘的独立集,因为它包含在G‘的空子图{1,2,5}中。{1,2,5}是G‘的最大独立集。{1,4,5}和{2,3,5}也是G‘的最大独立集。

最大团问题可以用回溯法在O(n2^n)的时间内解决。首先设最大团为一个空团,往其中加入一个顶点,然后依次考虑每个顶点,查看该顶点加入团之后仍然构成一个团。程序中采用了一个比较简单的剪枝策略,即如果剩余未考虑的顶点数加上团中顶点数不大于当前解的顶点数,可停止回溯。用邻接矩阵表示图G,n为G的顶点数,cn存储当前团的顶点数,bestn存储最大团的顶点数。当cn+n-i<bestn时,不能找到更大的团,利用剪枝函数剪去。

#include<iostream>
using namespace std;
const int maxnum=101;
bool graph[maxnum][maxnum];
bool use[maxnum],bestuse[maxnum];
int cn,bestn,v,e;

void backtrack(int i)
{
    if(i>v)
    {
    	if(cn>bestn)
    	{
        	bestn=cn;
        	for(int j=1;j<=v;j++) bestuse[j]=use[j];
        	return;
    	}
    }
    bool flag=true;
    for(int j=1;j<i;j++)
    {
    	if(use[j]&&!graph[j][i])
        {
            flag=false;
            break;
        }
    }
    if(flag)
    {
        cn++;
        use[i]=true;
        backtrack(i+1);
        use[i]=false;
        cn--;
    }
    if(cn+v-i>bestn)
    {
        use[i]=false;
        backtrack(i+1);
    }
}

int main()
{
    cin>>v>>e;
    for(int i=1;i<=e;i++)
    {
    	int p1,p2;
    	cin>>p1>>p2;
  		graph[p1][p2]=true;
  		graph[p2][p1]=true;
    }
    backtrack(1);
    cout<<bestn<<endl;
    for(int i=1;i<=v;i++)
	{
		if(bestuse[i]) cout<<i<<" ";
	}
    cout<<endl;
}

8.圆排列问题

给定n个大小不等的圆c1,c2,…,cn,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为

注意,下面代码中圆排列的圆心横坐标以第一个圆的圆心为原点。所以,总长度为第一个圆的半径+最后一个圆的半径+最后一个圆的横坐标。

#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
float minlen=10000,x[4],r[4];//当前最优值,当前圆排列圆心横坐标,当前圆排列
int n;//圆排列中圆的个数

//计算当前所选择圆的圆心横坐标
float center(int t)
{
    float temp=0;
	for(int j=1;j<t;j++)
	{
		//由x^2=sqrt((r1+r2)^2-(r1-r2)^2)推导而来
        float valuex=x[j]+2.0*sqrt(r[t]*r[j]);
		if(valuex>temp) temp=valuex;
    }
    return temp;
}

//计算当前圆排列的长度
void compute()
{
    float low=0,high=0;
	for(int i=1;i<=n;i++)
	{
		if(x[i]-r[i]<low) low=x[i]-r[i];
        if(x[i]+r[i]>high) high=x[i]+r[i];
    }
    if(high-low<minlen) minlen=high-low;
}

void backtrack(int t)
{
    if(t>n) compute();
    else
	{
		for(int j=t;j<=n;j++)
		{
			swap(r[t],r[j]);
			float centerx=center(t);
			if(centerx+r[t]+r[1]<minlen)
			{
				x[t]=centerx;
				backtrack(t+1);
			}
			swap(r[t],r[j]);
		}
	}
}

int main()
{
	n=3;
	r[1]=1,r[2]=1,r[3]=2;
	cout<<"各圆的半径分别为:"<<endl;
	for(int i=1;i<=3;i++) cout<<r[i]<<" ";
	cout<<endl;
	cout<<"最小圆排列长度为:";
	backtrack(1);
	cout<<minlen<<endl;
}

上述算法尚有许多改进的余地。例如,像1,2,…,n-1,n和n,n-1, …,2,1这种互为镜像的排列具有相同的圆排列长度,只计算一个就够了。而且,如果所给的n个圆中有k个圆有相同的半径,则这k个圆产生的k!个完全相同的圆排列,也只需要计算一个。

9.连续邮资问题

假设国家发行了k种不同面值的邮票,并且规定每张信封上最多只允许贴h张邮票。连续邮资问题要求对于给定的k和h的值,给出邮票面值的最佳设计,在1张信封上可贴出从邮资1开始,增量为1的最大连续邮资区间。例如,当k=5和h=4时,面值为(1,3,11,15,32)的5种邮票可以贴出邮资的最大连续邮资区间是1到70。UVA165就是一道这样的典型例题。用stampval来保存各个面值,用maxval来保存当前所有面值能组成的最大连续面值。那么,stampval[0] 一定等于1,因为1是最小的正整数。相应的,maxval[0]=1*h。接下去就是确定第二个,第三个......第k个邮票的面值了。对于stampval[i+1],它的取值范围是stampval[i]+1~maxval[i]+1。 stampval[i]+1是因为这一次取的面值肯定要比上一次的面值大,而这次取的面值的上限是上次能达到的最大连续面值+1,
是因为如果比这个更大的话, 那么就会出现断层, 即无法组成上次最大面值+1这个数了。 举个例子, 假设可以贴3张邮票,有3种面值,前面2种面值已经确定为1,2, 能达到的最大连续面值为6, 那么接下去第3种面值的取值范围为3~7。如果取得比7更大的话会怎样呢? 动手算下就知道了,假设取8的话, 那么面值为1,2,8,将无法组合出7。直接递归回溯所有情况, 便可知道最大连续值了。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#define MAXN 200
using namespace std;
int h,k,ans[MAXN],stampval[MAXN],maxval[MAXN],maxstampval;
bool vis[MAXN];  

//标记每种取到的钱数
void mark(int n,int m,int sum)
{
    if(m>h) return;
	vis[sum]=true;
    for(int i=1;i<=n;++i) mark(n,m+1,sum+stampval[i]);
}  

void backtrack(int cur)
{
    if(cur>k)
	{
        if(maxval[cur-1]>maxstampval)
		{
            maxstampval=maxval[cur-1];
            memcpy(ans,stampval,sizeof(stampval));
        }
        return;
    }
    for(int i=stampval[cur-1]+1;i<=maxval[cur-1]+1;++i)
	{
        memset(vis,0,sizeof(vis));
        stampval[cur]=i;
        mark(cur,0,0);
        int num=0,j=1;
        while(vis[j++]) ++num;
        maxval[cur]=num;
        backtrack(cur+1);
    }
}  

int main()
{
	while(scanf("%d %d",&h,&k),h+k)
	{
		maxval[1]=h;
        stampval[1]=1;
        maxstampval=-1;
        backtrack(2);
        for(int i=1;i<=k;++i) printf("%3d",ans[i]);
        printf("->%3d\n",maxstampval);
    }
} 

直接递归的求解复杂度太高,不妨尝试计算用不超过m张面值为x[1:i]的邮票贴出邮资k所需的最少邮票数y[k]。通过y[k]可以很快推出r的值。事实上,y[k]可以通过递推在O(n)时间内解决。这里就不再讲解了。

10.符号三角形问题

下图是由14个“+”和14个“-”组成的符号三角形,第一行有n个符号。2个同号下面都是“+”,2个异号下面都是“-”。

符号三角形问题要求对于给定的n,计算有多少个不同的符号三角形,使其所含的“+”和“-”的个数相同。在第1行前i个符号x[1:i]确定后,就确定了1个由i(i+1)/2个符号组成的三角形。下一步确定第i+1个符号后,在右边再加1条边,就可以扩展为前i+1个符号x[1:i+1]对应的新三角形。这样依次扩展,直到x[1:n]。最终由x[1:n]所确定的符号三角形中含"+"号个数与"-"个数同为n(n+1)/4。因此,当前符号三角形所包含的“+”个数与“-”个数均不超过n*(n+1)/4,可以利用这个条件剪支。对于给定的n,当n*(n+1)/2为奇数时,显然不存在包含的"+"号个数与"-"号个数相同的符号三角形。在回溯前需要简单的判断一下。

#include<iostream>
using namespace std;
int n,half,counts,p[100][100],sum;
//第一行的符号个数,n*(n+1)/4,当前"+"号个数,符号三角矩阵,已找到的符号三角形数               

void backtrack(int t)
{
    if((counts>half)||(t*(t-1)/2-counts>half)) return;
    if(t>n) sum++;
    else
    {
		for(int i=0;i<2;i++)
        {
            p[1][t]=i;//第一行符号
            counts+=i;//当前"+"号个数
            for(int j=2;j<=t;j++)
            {
                p[j][t-j+1]=p[j-1][t-j+1]^p[j-1][t-j+2];
                counts+=p[j][t-j+1];
            }
            backtrack(t+1);
            for(int j=2;j<=t;j++)
            {
                counts-=p[j][t-j+1];
            }
            counts-=i;
        }
    }
}   

int main()
{
	cin>>n;
	half=n*(n+1)/2;
    if(half%2==1)
    {
		cout<<"共有"<<sum<<"个不同的符号三角形。"<<endl;
		return 0;
    }
    half=half/2;
    backtrack(1);
    cout<<"共有"<<sum<<"个不同的符号三角形。"<<endl;
} 

关于回溯算法的基础知识就简要介绍到这里,希望能作为大家继续深入学习的基础。

时间: 2024-10-08 20:30:08

从零开始学回溯算法的相关文章

从零开始学贪心算法

本文在写作过程中参考了大量资料,不能一一列举,还请见谅. 贪心算法的定义: 贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解.贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关. 解题的一般步骤是: 1.建立数学模型来描述问题: 2.把求解的问题分成若干个子问题: 3.对每一子问题求解,得到子问题的局部最优解: 4.把子

从零开始学正则(四)

壹 ? 引 我在从零开始学正则(三)这篇博客中介绍了分组引用与反向引用的概念,灵活利用分组能让我们的正则表达式更为简洁.在文章结尾我们留下了两个问题,一问使用正则模拟实现 trim方法:二问将my name is echo每个单词首字母转为大写. 我们先来分析第一个问题,trim属于String方法,能去除字符串头尾空格,所以我们只需要写一个正则匹配头尾空格将其替换成空即可.空格属于空白符,所以这里需要使用字符组  \s ,空格可能有多个,所以要使用量词 + :其次我们需要匹配紧接开头后与结尾前

零基础学贪心算法

本文在写作过程中参考了大量资料,不能一一列举,还请见谅.贪心算法的定义:贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解.贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关.解题的一般步骤是:1.建立数学模型来描述问题:2.把求解的问题分成若干个子问题:3.对每一子问题求解,得到子问题的局部最优解:4.把子问题的局部最优

IC卡解密从零开始学2 版本更新! 解密工具PN532-mfoc-mfcuk-GUI V2.1 By:lookyour

程序更新 更新内容最下面 2017/5/3 V2.1====================================== 最简要介绍下M1卡数据结构 目前能看到的有2种M1卡,分别为S50 S70,其实就是容量不一样, S50为1Kbyte  S70为4Kbyte主要介绍S50 即1K卡   4K卡很少见 S50容量1Kbyte,16个扇区(Sector),每个扇区4块(Block)(块0-3),共64块,按块号编址为0-63.每个扇区有独立的一组密码及访问控制.第0扇区的块0(即绝对地

第1次实验——NPC问题(回溯算法、聚类分析)

题目:八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例.该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一列或同一斜线上,问有多少种摆法. 高斯认为有76种方案.1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果.计算机发明后,有多种方法可以解决此问题.     请编程实现八皇后问题,并把92种解的前三种解输出到屏幕(8*8的二维矩阵,Q代表皇后,X代

【高德地图API】从零开始学高德JS API(一)地图展现

摘要:关于地图的显示,我想大家最关心的就是麻点图,自定义底图的解决方案了吧.在过去,marker大于500之后,浏览器开始逐渐卡死,大家都开始寻找解决方案,比如聚合marker啊,比如麻点图啊.聚合marker里面还有一些复杂的算法,而麻点图,最让大家头疼的,就是如何生成麻点图,如何切图,如何把图片贴到地图上,还有如何定位图片的位置吧.以前那么复杂的一系列操作,居然让云图的可视化操作一下子解决了.现在只要点一点鼠标,麻点图就自动生成了.真是广大LBS开发者的福音. 以前写过从零开始学百度地图AP

【高德地图API】从零开始学高德JS API(六)——坐标转换

原文:[高德地图API]从零开始学高德JS API(六)--坐标转换 摘要:如何从GPS转到谷歌?如何从百度转到高德?这些都是小case.我们还提供,如何将基站cell_id转换为GPS坐标? ----------------------------------------------------------------------------------------- 第一部分 各种坐标系详解 1.大地坐标系统 WGS-84 用来表述地球上点的位置的一种地区坐标系统.它采用一个十分近似于地球自

79. 单词搜索-回溯算法(leetcode)

给定一个二维网格和一个单词,找出该单词是否存在于网格中. 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格.同一个单元格内的字母不允许被重复使用. 想法:本题跟我们9021 quiz7-8的类型是一样的,9024也用C写过一次,都是在二维数组里搜索,用回溯算法,今天脑袋有点不清醒,以后多刷几次. 学到的点: 1. 每一步递归中,都要注意判断 start_x,start_y的取值范围 2. 学到了python里面的语法还可以直接大于小于判断.

8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,循环控制及其优化

上两篇博客 8皇后以及N皇后算法探究,回溯算法的JAVA实现,递归方案 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,数据结构“栈”实现 研究了递归方法实现回溯,解决N皇后问题,下面我们来探讨一下非递归方案 实验结果令人还是有些失望,原来非递归方案的性能并不比递归方案性能高 代码如下: package com.newflypig.eightqueen; import java.util.Date; /** * 使用循环控制来实现回溯,解决N皇后 * @author [email pr