算法期末复习总结 ||蛮力法(幂级空间、排列空间、图的深度优先、广度优先、时间复杂度分析)

蛮力法

1. 蛮力法的原理及特点

蛮力法是一种简单直接地解决问题的方法,通常直接基于问题的描述和所涉及的概念定义,找出所有可能的解。然后选择其中的一种或多种解,若该解不可行则试探下一种可能的解。

蛮力法通常用于

  1. 搜索所有解空间:问题的解存在于规模不大的解空间中。
  2. 搜索所有的路径:这类问题中不同的路径对应不同的解。
  3. 直接计算:按照基于问题的描述和所涉及的概念定义,直接进行计算。往往是一些简单的题,不需要算法技巧的。
  4. 模拟和仿真:按照求解问题的要求直接模拟或仿真即可。

2. 蛮力法的基本应用示例,掌握幂级空间和排列空间的构造

【基本应用示例】

1. 字符串匹配问题

点击跳转

2. 最大子序列和

点击跳转

【幂级空间】

1. 求解幂级问题

问题描述

对于给定的正整数n(n≥1),求1~n构成的集合的所有子集(幂集)。

问题求解
解法1:直接蛮力法
将 1~n 存放在数组b中,求解问题变为构造集合b的所有子集。

对于含有n(n≥1)个元素的集合a,求幂集的过程如下:
for (i=0;i<2n;i++) //穷举a的所有子集并输出
{
将i转换为二进制数b;
输出b中为1的位对应的a元素构成一个子集;
}

代码

//求解幂级问题
//对于给定的正整数n(n≥1),求1~n构成的集合的所有子集(幂集)。
//1. 采用直接蛮力法求解
#include<iostream>
#include<cmath>
using namespace std;
#define MAXN 20

void Bin(int a[],int n) //将a表示的二进制数增1
{
	for (int i = 0; i < n; i++)
	{
		if (a[i]) //将元素1改为0
			a[i] = 0;
		else //将元素0改为1并退出for循环
		{
			a[i] = 1;
			break;
		}
	}
}

void Solve(int a[], int b[], int n)
{
	int pw = pow(2, n); //求2^n
	cout << "幂集为:";
	for (int i = 0; i < pw; i++) //执行2^n次
	{
		cout << "{";
		for(int j=0;j<n;j++) //执行n次
			if (a[j])
			{
				cout << b[j] << " ";
			}
		cout << "}";
		Bin(a, n);
	}
	cout << endl;
}

int main()
{
	int a[MAXN], b[MAXN];
	int n;
	cout << "请输入n:";
	cin >> n;
	cout << "请依次输入数字:";
	for (int i = 0; i < n; i++)
		cin >> b[i];
	Solve(a, b, n);
	system("pause");
	return 0;
}

算法分析
显然该算法的时间复杂度为O(n×2n),属于指数级的算法。

解法2:增量蛮力法
问题求解
即穷举1~n的所有子集。先建立一个空子集,对于i(1≤i≤n),每次都是在前面已建立的子集上添加元素i而构成若干个子集。

void f(int n) //求1~n的幂集ps
{
 置ps={{}}; //在ps中加入一个空子集元素
 for (i=1;i<=n;i++)
  在ps的每个元素中添加i而构成一个新子集;
}

代码

#include<iostream>
#include<vector>
using namespace std;
vector<vector<int> > ps; //存放幂集

void PSet(int n) //求1~n的幂集ps
{
	vector<vector<int> > ps1; //子幂集
	vector<vector<int> >::iterator it;//幂集迭代器
	vector<int> s;
	vector<int>::iterator sit;

	ps.push_back(s); //添加{}空集合元素
	for (int i = 1; i <= n; i++) //循环添加1~n
	{
		ps1 = ps; //ps1存放上一步得到的幂集
		for (it = ps1.begin(); it != ps1.end(); ++it)
			(*it).push_back(i); //在ps1的每个集合元素末尾添加i
		for (it = ps1.begin(); it != ps1.end(); ++it)
			ps.push_back(*it); //将ps1的每个集合元素添加到ps中
	}
	for (it = ps.begin(); it != ps.end(); it++) {
		cout << "{";
		for (sit = (*it).begin(); sit != (*it).end(); sit++)
			cout << " " << *sit << " ";
		cout << "}";
	}
	cout << endl;
}

int main()
{
	int n;
	cout << "请输入n:";
	cin >> n;
	cout << "1~" << n << "的幂集为:";
	PSet(n);
	system("pause");
	return 0;
}

算法详解
对于给定的n,每一个集合元素都要处理,有2n个,所以上述算法的时间复杂度为O(2n)。

2. 求解0/1背包问题

问题描述
有n个重量分别为{w1,w2,…,wn}的物品,它们的价值分别为{v1,v2,…,vn},给定一个容量为W的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且具有最大的价值。并对下表所示的4个物品求出W=6时的所有解和最佳解。

物品编号重量价值
154
234
323
411

问题求解
对于n个物品、容量为W的背包问题,采用前面求幂集的方法求出所有的物品组合。
对于每一种组合,计算其总重量sumw和总价值sumv,当sumw小于等于W时,该组合是一种解,并通过比较将最佳方案保存在maxsumw和maxsumv中,最后输出所有的解和最佳解。
代码

#include<iostream>
#include<vector>
using namespace std;

vector<vector<int> > ps; //存放幂集

void pSet(int n)
{
	vector<vector<int> >ps1; //存放子幂集
	vector<vector<int> >::iterator it; //子幂集迭代器
	vector<int> s;
	ps.push_back(s); //添加{ }空集合元素

	for (int i = 1; i <= n; i++)
	{
		ps1 = ps; //ps1存放上一步存放的幂集
		for (it = ps1.begin(); it != ps1.end(); ++it) //在ps1上的每个集合元素末尾添加i
		{
			(*it).push_back(i);
		}
		for (it = ps1.begin(); it != ps1.end(); ++it) //将ps1的每个集合元素添加到ps中
		{
			ps.push_back(*it);
		}
	}
}

void Knap(int w[], int v[], int W) //求所有方案和最佳方案
{
	int count = 0; //方案编号
	int sumw, sumv; //当前方案的总重量和总价值
	int maxi, maxsumw = 0, maxsumv = 0; //最优方案的编号,总重量和总价值
	vector<vector<int> >::iterator it; //幂集迭代器
	vector<int>::iterator sit; //幂集集合元素迭代器
	cout << " 序号\t选中物品\t总重量\t总价值\t能否装入" << endl;
	for (it = ps.begin(); it != ps.end(); ++it)
	{
		cout << count + 1 << "\t";
		sumw = sumv = 0;
		cout << "{";
		for (sit = (*it).begin(); sit != (*it).end(); ++sit)
		{
			cout << *sit;
			sumw += w[*sit - 1]; //w[]数组从下标0开始
			sumv += v[*sit - 1]; //v[]数组从下标0开始
		}
		cout << "}\t\t" << sumw << "\t" << sumv << "\t";
		if (sumw <= W)
		{
			cout << "Yes" << endl;
			if (sumv > maxsumv) //比较求出最优方案
			{
				maxsumw = sumw;
				maxsumv = sumv;
				maxi = count;
			}
		}
		else
			cout << "No" << endl;
		count++; //方案编号加1			
	}
	cout << "最佳方案为: ";
	cout << "选中物品";
	cout << "{";
	for (sit = ps[maxi].begin(); sit != ps[maxi].end(); ++sit)
		cout << *sit;
	cout << "}";
	cout << "总重量:" << maxsumw << "总价值:" << maxsumv;
}

int main()
{
	int n = 4, W = 6;
	int w[] = { 5,3,2,1 };
	int v[] = { 4,4,3,1 };
	pSet(n);
	printf("0/1背包的求解方案\n", n);
	Knap(w, v, W);
	system("pause");
	return 0;
}

运行截图
在这里插入图片描述

【排列空间】

1. 求解全排列问题

问题描述
对于给定的正整数n(n≥1),求1~n的所有全排列。
问题求解
这里采用增量蛮力法求解。产生1~3全排列的过程如下:

1
1,2
2,1
1,2,3
1,3,2
3,2,1
2,1,3
2,3,1
3,2,1

代码

#include<iostream>
#include<vector>
using namespace std;

vector<vector<int> > ps; //存放全排列

void Insert(vector<int> s, int i, vector<vector<int> > &ps1)
{
	//在每个集合元素中间插入i得到ps1
	vector<int> s1;
	vector<int>::iterator it;
	for (int j = 0; j < i; j++)
	{
		s1 = s;
		it = s1.begin() + 1; //求出插入位置
		s1.insert(it, i);
		ps1.push_back(s1); //添加到ps1中
	}
}

//permutation [数]排列、置换
void Perm(int n)
{
	vector<vector<int> > ps1;
	vector<vector<int> >::iterator it;
	vector<int> s, s1;
	vector<int>::iterator sit;
	s.push_back(1);
	ps.push_back(s); //添加{1}集合元素
	for (int i = 2; i <= n; i++)
	{
		ps1.clear();
		for (it = ps.begin(); it != ps.end(); ++it)
			Insert(*it, i, ps1);
		ps = ps1;
		for (it = ps.begin(); it != ps.end(); ++it)
		{
			cout << "{";
			for (sit = (*it).begin(); sit != (*it).end(); sit++)
				cout << " " << *sit << " ";
			cout << "}";
		}
	}
}

int main()
{
	int n = 3;
	cout << n << "的全排:" << endl;
	Perm(n);
	cout << endl;
	system("pause");
	return 0;
}

算法分析
对于给定的正整数n,每一种全排列都必须处理,有n!种,所以上述算法的时间复杂度为O(n*n!)。

2. 求解任务分配问题

问题描述
有n(n≥1)个任务需要分配给n个人执行,每个任务只能分配给一个人,每个人只能执行一个任务。第i个人执行第j个任务的成本是c[i][j](1≤i,j≤n)。求出总成本最小的分配方案。
问题求解
所谓一种分配方案就是由第i个人执行第j个任务,用(a1,a2,…,an)表示,即第1个人执行第a1个任务,第2个人执行第a2个任务,以此类推。全部的分配方案恰好是1~n的全排列。
这里采用增量穷举法求出所有的分配方案ps(全排列),再计算出每种方案的成本,比较求出最小成本的方案,即最优方案。
代码

#include<iostream>
#include<vector>
using namespace std;
#define MAXN 105
#define INF 0x3f3f3f3f
int n = 4;
int c[MAXN][MAXN] = { {9,2,7,8},{6,4,3,7},{5,8,1,8},{7,6,9,4} };
vector<vector<int> > ps; //存放全排列

void Insert(vector<int> s, int i, vector<vector<int> > &ps1)
{
	vector<int> s1;
	vector<int>::iterator it;
	for (int j = 0; j < i; j++)
	{
		s1 = s;
		it = s1.begin() + 1;
		s1.insert(it, i);
		ps1.push_back(s1);
	}
}

void Perm(int n)
{
	vector<vector<int> > ps1;
	vector<vector<int> >::iterator it;
	vector<int> s, s1;
	s.push_back(1);
	ps.push_back(s);
	for (int i = 2; i <= n; i++)
	{
		ps1.clear();
		for (it = ps.begin(); it != ps.end(); ++it)
			Insert(*it, i, ps1);
		ps = ps1;
	}
}

void Allocate(int n, int &mini, int &minc)
{
	Perm(n);
	for (int i = 0; i < ps.size(); i++)
	{
		int cost = 0;
		for(int j=0;j<ps[i].size();j++)
			cost += c[j][ps[i][j] - 1];
		if (cost < minc)
		{
			minc = cost;
			mini = i;
		}
	}
}

int main()
{
	int mincost = INF, mini;
	Allocate(n, mini, mincost);
	printf("最优方案:\n");
	for (int k = 0; k < ps[mini].size(); k++)
		printf(" 第%d个人安排任务%d\n", k + 1, ps[mini][k]);
	printf(" 总成本=%d\n", mincost);
	system("pause");
	return 0;
}

3. 图的深度优先和广度优先遍历算法

图的存储结构:邻接矩阵、邻接表
邻接矩阵的存储方法
在这里插入图片描述

邻接矩阵的类型定义

//邻接矩阵的类型定义
#define MAXV 20
#define MAXL 15

typedef struct
{
	int no; //顶点编号
	char data[MAXL]; //顶点其他信息
} VertexType; // 顶点类型||vertex [数]顶点

typedef struct
{
	int edges[MAXV][MAXV]; //邻接矩阵的边数组
	int n, e; //顶点数、边数
	VertexType vexs[MAXV]; //存放顶点信息
} MGraph; //完整的图邻接矩阵类型

邻接表存储方法
图的邻接表存储方法是一种链式存储结构。图的每个顶点建立一个单链表,第i(0≤i≤n-1)个单链表中的结点表示依附于顶点i的边。每个单链表上附设一个表头结点,将所有表头结点构成一个表头结点数组。
在这里插入图片描述

邻接表的类型定义

//邻接表的类型定义
typedef struct ANode
{
	int adjvex; //该边的终点编号
	int weight; //该边的权值
	struct ANode *nextarc; //指向下一条边的指针
} ArcNode; //边结点类型

typedef struct Vnode
{
	char data[MAXL]; //顶点其他信息
	ArcNode *firstarc; //指向下一条边
} VNode; //邻接表头结点类型

typedef VNode AdjList[MAXV]; //AdiList是邻接表类型

typedef struct
{
	AdjList adjlist; //邻接表
	int n, e; //图中的顶点数和边数
} ALGraph;

【深度优先遍历】

图的遍历:从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次。
深度优先搜索的过程
(1)从图中某个初始顶点v出发,首先访问初始顶点v。
(2)然后选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索。
(3)重复直到图中与当前顶点v邻接的所有顶点都被访问过为止。显然,这个搜索过程是个递归过程。

1. 邻接表求解简单路径

问题描述
假设图G采用邻接表存储,设计一个算法判断图G中从顶点u到v是否存在简单路径。
问题求解
所谓简单路径是指路径上的顶点不重复,采用深度优先遍历的方法。
代码

#include<iostream>
#include<vector>
using namespace std;

//邻接矩阵的类型定义
#define MAXV 20
#define MAXL 15
#define INF 0x3f3f3f3f
int visited[MAXV]; //访问数组


//邻接表的类型定义
typedef struct ANode
{
	int adjvex; //该边的终点编号
	int weight; //该边的权值
	struct ANode *nextarc; //指向下一条边的指针
} ArcNode; //边结点类型

typedef struct Vnode
{
	char data[MAXL]; //顶点其他信息
	ArcNode *firstarc; //指向下一条边
} VNode; //邻接表头结点类型

typedef VNode AdjList[MAXV]; //AdiList是邻接表类型

typedef struct
{
	AdjList adjlist; //邻接表
	int n, e; //图中的顶点数和边数
} ALGraph;

//判断G中从顶点u到v是否存在简单路径
bool ExistPath(ALGraph *G, int u, int v)
{
	int w;
	ArcNode *p;
	visited[u] = 1; //置已访问标记
	if (u == v) //找到一条路径,返回true
		return true;
	p = G->adjlist[u].firstarc; //p指向顶点u的第一个相邻点
	while (p != NULL)
	{
		w = p->adjvex; //w为顶点u的相邻顶点
		if (visited[w] == 0) //若w顶点未被访问,则递归访问它
		{
			bool flag = ExistPath(G, w, v);
			if (flag) return true;
		}
		p = p->nextarc; //p指向顶点u的下一个相邻点
	}
	return false; //如果没有找到,返回false
}


void CreateAdj(ALGraph *&G, int a[][MAXV], int n, int e) { //建立图的邻接表
	ArcNode *p;
	G = (ALGraph *)malloc(sizeof(ALGraph));
	G->n = n;
	G->e = e;
	for (int k = 0; k < n; k++)
		G->adjlist[k].firstarc = NULL;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
			if (a[i][j] != 0 && a[i][j] != INF) {
				p = (ArcNode *)malloc(sizeof(ArcNode));
				p->adjvex = j;
				p->weight = a[i][j];
				p->nextarc = G->adjlist[i].firstarc;
				G->adjlist[i].firstarc = p;
			}
}

void DispAdj(ALGraph *G) {
	ArcNode *p;
	for (int i = 0; i < G->n; i++) {
		cout << i;
		p = G->adjlist[i].firstarc;
		while (p != NULL) {
			cout << "->" << "结点编号:" << p->adjvex << " 边的权值:" << p->weight;
			p = p->nextarc;
		}
		cout << "^" << endl;
	}
}

void DestroyAdj(ALGraph *&G) { // 销毁图的邻接表
	ArcNode * pre, *p;
	for (int i = 0; i < G->n; i++) {
		pre = G->adjlist[i].firstarc;
		while (pre != NULL) {
			p = pre->nextarc;
			free(pre);
			pre = p;
		}
	}
	free(G);
}



int main()
{
	ALGraph *G;
	int a[MAXV][MAXV] ={ {0,1,0,0,1},{1,0,1,1,1},{0,1,0,1,0},{0,1,1,0,1},{1,1,0,1,0} };
	int n = 5, e = 7;
	CreateAdj(G, a, n, e); //创建图的邻接表存储结构G
	cout << "图的邻接表如下:" << endl;
	DispAdj(G);
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++) {
			memset(visited, 0, sizeof(visited)); // 对数组visited进行初始化
			if (ExistPath(G, i, j) && i != j)
				cout << "顶点" << i << "到顶点" << j << "存在简单路径" << endl;
		}
	DestroyAdj(G);
	system("pause");
	return 0;
}

【广度优先遍历】

广度优先搜索的过程是:
(1)首先访问初始顶点v。
(2)接着访问顶点v的所有未被访问过的邻接点v1,v2,…,vt。
(3)然后再按照v1,v2,…,vt的次序,访问每一个顶点的所有未被访问过的邻接点,依次类推,直到图中所有和初始顶点v有路径相通的顶点都被访问过为止。

邻接矩阵为图的存储结构,采用广度优先搜索图:

void BFS(MGraph g, int v) //邻接矩阵的BFS算法
{
	queue<int> qu; //定义一个队列qu
	int visited[MAXV]; //定义存放结点的访问标志的数组
	int w, i;
	memset(visited, 0, sizeof(visited)); //访问标志数组初始化
	printf("%3d", v); //输出被访问顶点的编号
	visited[v] = 1; //置已访问标记
	qu.push(v); //v进队
	while (!qu.empty()) //队列不空时循环
	{
		w = qu.front(); qu.pop(); //出队顶点w
		for (i = 0; i < g.n; i++) //找与顶点w相邻的顶点
			if (g.edges[w][i] != 0 && g.edges[w][i] != INF && visited[i] == 0)
			{ //若当前邻接顶点i未被访问
				printf("%3d", i); //访问相邻顶点
				visited[i] = 1; //置该顶点已被访问的标志
				qu.push(i); //该顶点进队
			}
	}
	printf("\n");
}

邻接表为图的存储结构,采用广度优先搜索图:

void BFS(ALGraph *G, int v) //邻接表的BFS算法
{
	ArcNode *p;
	queue<int> qu; //定义一个队列qu
	int visited[MAXV], w; //定义存放顶点的访问标志的数组
	memset(visited, 0, sizeof(visited)); //访问标志数组初始化
	printf("%3d", v); //输出被访问顶点的编号
	visited[v] = 1; //置已访问标记
	qu.push(v); //v进队
	while (!qu.empty()) //队列不空时循环
	{
		w = qu.front(); qu.pop(); //出队顶点w
		p = G->adjlist[w].firstarc; //找顶点w的第一个邻接点
		while (p != NULL)
		{
			if (visited[p->adjvex] == 0) //若当前邻接顶点未被访问
			{
				printf("%3d", p->adjvex); //访问相邻顶点
				visited[p->adjvex] = 1; //置该顶点已被访问的标志
				qu.push(p->adjvex); //该顶点进队
			}
			p = p->nextarc; //找顶点w的下一个邻接点
		}
	}
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值