目录
贪心算法课本练习
第1关:单源最短路径问题
回溯法进阶练习
先粗略的介绍一下回溯法:
回溯法的特征:
递归回溯一般算法框架:
第1关:子集和问题
第2关:最小长度圆排列
分析:计算该排列每个圆的圆心坐标
第3关:工作分配问题
本关任务:编写程序采用贪心算法计算单源最短路径。
为了完成本关任务,你需要掌握:1.如何使用邻接矩阵表示图,2.如何设计贪心选择策略将问题规模逐渐变小来解决单源最短路径问题。
单独设计Dijkstra算法(函数)来求解单源最短路径问题,在Main函数中初始化一个带权有向图的临界矩阵,并调用函数Dijkstra()求解并输出结果。说明,Main函数已经给出初始化的邻接矩阵c[][],为了便于计算,将邻接矩阵c[][]的0行和0列初始化为0,矩阵从c[1][1]到c[n][n]区域表达顶点v1、v2、...vn的邻接关系。
平台会对你编写的代码进行测试:
测试输入:Main函数已经初始化邻接矩阵;
预期输出: 输出结果,按照如下示例格式:
0 10 50 30 60 2<-1 3<-4<-1 4<-1 5<-3<-4<-1 其中,第一行表示顶点v1分别到v1、v2、v3、v4、v5的单源最短路径长度,v1到自身路径长度为0。第2行表示v1到v2的最短路径,第3行表示v1到v3的最短路径,第4行表示v1到v4的最短路径,第5行表示v1到v5的最短路径。
Dijkstra的主要思想:
设置顶点集合S,并不断的做贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短长度已知。初始时,S中仅含有源。设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路径称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到s中,同时对数组dist做出必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点之间的最短路径长度。
c[i][j]表示边(i,j)的权。prev[i]记录的是从源到顶点i的最短路径上i的前一个顶点。dist[i]表示当前从源到顶点i的最短特殊路径长度。
#include
#include
#include
using namespace std;
#define maxint 9999
template
void Dijkstra(int n, int v, Type dist[], int prev[], Type c[][6])
{
//初始化操作
bool s[6];//表示点是否在集合中
for (int i = 1; i <= n; i++)
{
dist[i] = c[v][i];
s[i] = false;//初始化时,除了v外所有点均不在s集合中
//判断结点是否可达 ,不可达前驱结点设置为0,可达前驱结点为源点
if (dist[i] == maxint)
{
prev[i] = 0;
}
else
{
prev[i] = v;
}
}
dist[v] = 0; s[v] = true;//设置源点V到自身的距离为0,将v加入到S顶点集合中
/*初始化完成*/
//
for (int i = 1; i < n; i++)
{
int temp = maxint;
int u = v;
for (int j = 1; j <= n; j++)//求离出发点最近的一个顶点
{
if ((!s[j]) && (dist[j] < temp))//点j在s中且距离出发点最近
{
u = j;
temp = dist[j];
}
}
s[u] = true;//将该点加入s
for (int j = 1; j <= n; j++)//s集合加入新点的时候需要更新dist[]和prev[]
{
if ((!s[j]) && (c[u][j] < maxint))//如果j点不在s中,且新点与j点相邻
{
Type newdist = dist[u] + c[u][j];//新点到v的距离+新点到j的距离
if (newdist < dist[j])//判断距离是不是需要更新
{
dist[j] = newdist;
prev[j] = u;//新点成了j的前驱结点,表示此时从v到j点经过u距离最短
}
}
}
}
//输出结果
for (int i = v; i <= n; i++)
{
cout << dist[i] << " ";
}
cout << endl;
for (int i = 2; i <= n; i++)
{
cout << i;
int j = i;
while (j != 1)
{
cout << "<-" << prev[j];
j = prev[j];
}
cout << endl;
}
}
int main()
{
int c[6][6] = { {0},{0,0,10,maxint,30,100},
{0,maxint,0,50,maxint,maxint},
{0,maxint,maxint,0,maxint,10},
{0,maxint,maxint,20,0,60},
{0,maxint,maxint,maxint,maxint,0} };
int dist[6] = { 0 };
int prev[6] = { 0 };
Dijkstra(5, 1, dist, prev, c);
return 0;
}
当需要找到问题的解集火之石要求满足某些约束条件的最优解时,常使用回溯法。
回溯法是具有剪枝函数的深度优先生成树,也是一种暴力解法。
深度优先搜索算法(DFS)的基本思想是:
(1)某一种可能情况向前探索,并生成一个子节点。
(2)过程中,一旦发现原来的选择不符合要求,就回溯至父亲结点,然后重新选择另一方向,再次生成子结点,继续向前探索。
(3)如此反复进行,直至求得最优解。
回溯法基本思想是:
(1)针对具体问题,定义问题的解空间(所有可能得解集合);
(2)确定易于搜索的解空间结构(也就是解空间树)。
(3)一般以DFS的方式搜索解空间,并在搜索过程中,可以使用剪枝函数来避免无效搜索。(剪枝函数:包括约束函数和限界函数。用约束函数-在扩展节点处剪去不满足约束的子树和限界函数--剪去得不到最优解的子树)
对子集树这两个函数约束函数用于左分支和限界函数用于右分支(在这里认为左分支是选上,右分支是不选);对于排列数,这两个函数同时应用于每一个分支,因为每一个分支的性质相同。
1.在搜索过程中动态产生问题的解空间树,即边搜索边扩展分支。不同于数据结构中的树的深度遍历法,先创建树,再深度遍历。
2.在任何时刻,算法只保存从根节点到当前扩展节点的路径。
通常我们把根节点看做是第0层节点,叶子结点是第n层节点。
void backtrace(int t)//参数t是当前扩展节点的下一层的编号
{
if(t>n) //当前扩展节点在第n层上,也就是已经走到叶子结点上
output(x);
else//通过for循环的控制依次展开这一层的所有分支
for(int i=f(n,t);i<=g(n,t);i++)//f(n,t)代表左分支编号,g(n,t)代表右分支编号
{
//for循环内部为对每一个分支的处理
x[t]=h(i);
if(constraint(t)&&bound(t))//通过约束函数和限界函数来判定生成的孩子节点是否满足要求
backtrace(t+1);
}
}
不理解的话可以在B站上找 算法设计与分析张公敬老师的课,解释的详细。
本关任务:子集和问题的一个实例为
,其中s={x1,x2,...,xn}
是一个正整数的集合,c
是一个正整数。子集和问题判定是否存在S
的一个子集S1
,使得∑x∈S1x=c
。试设计一个解子集和问题的回溯算法。
在右侧编写函数void dfs(int k)
。
它是一个递归函数,功能是设置解向量第k
个分量的取值。如果第k个分量取值为1,表示xk
加到子集中求和;第k个分量值为0,表示不选xk
。
数据的输入有两行:
数据的输出:
平台会对你编写的代码进行测试:
测试输入: 5
10
2
2
6
5
3
预期输出: 2
2
6
代码:
#include
using namespace std;
int s[10000];//存放s集合中每个元素
int x[10000];//解向量
int n,c;//n是输入元素个数,c是子集和
int dfs(int k);
int dfs(int k)//
{
if (sum == c) return true;//找到一种结果就直接返回
if (k >= n) return false;//到达叶子结点且没有找到满足条件的解
if (sum + s[k] <= c)//判断是否满足条件
{
x[k] = 1;//符合条件,选上
sum = sum + s[k];
if (dfs(k + 1))//循环左子树
return true;//找到一个解,直接返回
sum = sum - s[k];//回溯
}
if (dfs(k + 1))//循环右子树
return true;//找到一个解,直接返回
return false;
}
int main()
{
cin>>n>>c;
int flag;
for(int i = 0; i < n; i ++)
cin>>s[i];
flag = dfs(0);
if(flag==0)cout<<"No Solution!"<
给定n个大小不等的圆c1,c2,..,cn
,现要将圆放到矩形框内,且要求各圆与矩形框底边相切。圆排列问题要求从n
个圆的所有排列中找出有最小长度的圆排列。 例如:当n=3
,且所给的三个圆的半径分别是1,1,2
时,这3个圆的最小长度的圆排列如下图所示,最小长度是2+42=7.65685
.
两个圆相切,它们的圆心在横坐标上的距离: dist=(r1+r2)2−(r1−r2)2=2r1∗r2
设minx
为当前最小长度圆排列的长度,r=[r1,r2,...,rn]
是所给的n
个圆的半径,x=[x1,x2,...,xn]
为当前圆排列圆心横坐标,其中x1=0
.解圆排列问题的算法中,
void compute()
计算当前圆排列的长度,如果当前圆排列长度比minx
更小,则更新。double center(int t)
,计算第t个圆在当前排列中圆心的横坐标。void backtrack(int t)
是递归函数,它的功能是设置x[t]为合适的圆半径。
- 当t>n时,解向量全部设置好,直接调用compute计算全部圆排列长度,实时更新当前最优值minx;
- t>n不成立时,依次尝试x[t]~x[n]中圆作为第t个圆,如果当前t个圆排列长度比minx小,才可递归进入第t+1个圆的设置。
输入两行,第一行是圆的个数n,第二行是n个圆的半径。 输出1行,输出最小圆排列的长度,保留5位小数。 平台会对你编写的代码进行测试:
测试输入: 3
1
1
2
预期输出: 7.65685
分析:
计算该排列每个圆的圆心坐标如果两圆相切的话,可以利用公式dist=(r1+r2)2−(r1−r2)2=2r1∗r2
问题就来了:前一个圆和本圆一定一定相切吗?
那肯定不一定了,所以需要和前面所有的圆都进行一次计算。如果这个圆和前面相邻的圆并不相切,计算出来的结果是偏小的。所以我们需要把当前圆与前面所有的圆依次计算,保留最大的值,就是正确的值。
还有一个疑问:问什么求左右边界不直接用最左边最右边的圆的横坐标嘞?
看下图:可知最左边的圆的圆横坐标并不是左边界,右边也是一样的。所以我们需要用每个圆的圆心与半径来算出每个圆的左边界,并取最小就是整个排列的左边界了。右边界也同样,把每个圆的右边界算出来,取最大就是整个排列的右边界了。
一个小小的部分要考虑好多特殊的情况呀
代码:
#include
#include
using namespace std;
double r[1000];//当前圆排列
double x[1000];//当前圆排列圆心横坐标
double bestx[1000];//记录最优解向量
double minx;
int n;
void Compute()//计算当前圆排列的长度,如果当前圆排列长度比minx更小,则更新。
{
double 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 < minx)//更新最小值
{
minx = high - low;
for (int i = 1; i <= n; i++)
bestx[i] = r[i];
}
}
double center(int t)//计算第t个圆在当前排列中圆心的横坐标
{
double temp = 0;
for (int i = 1; i < t; i++)//因为无法保证前一个圆与本圆相切,故要与本圆相切圆
{
double value = 2.0 * sqrt(r[t] * r[i]) + x[i];
if (temp < value)
temp = value;
}
return temp;
}
void backtrack(int t)
{
if (t > n)//到达叶子结点,直接调用compute计算全部圆排列长度,实时更新当前最优值minx;
Compute();
else
{
for (int j = t; j <= n; j++)//依次尝试x[t]~x[n]中圆作为第t个圆
{
swap(r[t], r[j]);
double nowx = center(t);
if (nowx + r[t] + r[1] < minx)//剪枝条件
{
x[t] = nowx;
backtrack(t + 1);
}
swap(r[t], r[j]);//回溯
}
}
}
int main()
{
minx = 0x7fffffff;//初始赋值minx为最大值
cin >> n;
for (int i = 1; i <=n; i++)
{
cin >> r[i];
}
backtrack(1);
cout << minx << endl;
for (int i = 1; i <= n; i++)
cout << bestx[i] << " ";
return 0;
}
设有n件工作分配给n
个人。将工作i
分配给第j
个人所需的费用为cij
。试设计一个算法,为每个人都分配一件不同的工作,并使总费用达到最小。
无
设n
是要分配的人数和工作件数,cost[i][j]
是第i
件工作分配第j
个人的费用,x[]是当前解向量,ccost
是当前费用,mincost
是最小总费用,算法每次找到更小的总费用就更新它。 在右侧编辑函数void backtrack(int t)
:
功能是在第1...t-1
件工作已分配人,且放置到x[1]...x[t-1]
的基础上,设置第t件工作要分配的人x[t]
。它可选的人员编号在x[t]...x[n]
中,可依次尝试,如果当前费用ccost
已经超出目前得到的最小总费用mincost
,则跳过该尝试。
输入:第1行有1个正整数n(1≤n≤20)
,接下来n
行,每行n
个数,表示工作费用。 输出1行,为最小总费用。 平台会对你编写的代码进行测试:
测试输入: 3
10
2
3
2
3
4
3
4
5
预期输出: 9
代码:
#include
#include
using namespace std;
int n;
int cost[1000][1000];//费用表
int x[1000];//解向量
int ccost;//当前费用
int mincost;//最小费用
int bestx[1000];//记录最优解向量
void backtrack(int t)
{
if (t >n)//到达叶子结点
{
if (ccost < mincost)//更新最小费用
{
mincost = ccost;
for(int i=1;i<=n;i++)//不需要也可以直接删掉for循环
bestx[i]=x[i];
}
}
else
{
for (int i = t; i <=n; i++)//表示选择哪个任务
{
int f1 = cost[x[i]][t];//x[i]表示哪个人来完成
ccost = ccost + f1;//目前完成的时间
if (ccost< mincost)//满足限界函数
{
swap(x[t], x[i]);
backtrack(t + 1);
swap(x[t], x[i]);//回溯
}
ccost = ccost - f1;//回溯
}
}
}
int main()
{
cin >> n;
int temp=0;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> cost[i][j];
if (temp < cost[i][j])temp = cost[i][j];
}
}
mincost = temp*n;//mincost初始值
for (int i = 1; i <= n; i++)
x[i] = i;
backtrack(1);
cout << mincost << endl;
for(int i=1;i<=n;i++)
cout<
9
2 1 3