C/C++经典算法细解

一、算法基础知识

1.1 算法的概念

算法是对特定问题求解步骤的一种描述方式,是若干指令的有穷序列。(有限的时间或者空间内算出确定的结果)

1.2 算法的特性

  • 输入。有0个或多个输入,来源于外界提供或自己产生。
  • 输出。有一个或多个输出,算法目的是获取问题的解,没有输出的算法是无意义的。
  • 确定性。组成算法的每条指令必须有确定的含义,无歧义。任何条件下,相同的输入必有相同的结果。
  • 有限性。每一指令的执行次数是有限的,执行每条指令的时间也是有限的。

1.3 算法的描述方式

  • <<:左移运算符(<<)将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。例:x = x << 2 将a的二进制位左移2位,右补0,相当于x = x * 4;
  • >>:右移运算符(>>)将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。操作数每右移一位,相当于该数除以2。例如:x = x >> 2 将a的二进制位右移2位,左补0或补1得看被移数是正还是负。
  • &:二进制“与”(都为1时,结果是1,否则是0),比如:1010&1011=1010,1010&1000=1000。
  • |:二进制“或”(有1时,结果是1,都是0时,结果为0),比如:1010|1011=1011,1010|1000=1010。
  • ~:按位取反

1.4 算法设计的一般过程

  1. 充分理解要解决的问题;
  2. 数学模型拟制。算法设计的时候,首先根据问题的的描述,建立符合要求的数学的模型,用文字描述抽象的方法,并设计合适的约束的规则;
  3. 算法详细设计。将算法具体化,要选择算法的设计策略,并确定合理的数据结构;
  4. 算法描述。根据前三部分的工作,采用描述工具将算法的具体过程描述下来;
  5. 算法思路的正确性验证。通过一系列与算法的工作对象有关的引理、定理和公式进行证明,来验证算法所选择的设计策略及设计思路是否正确;
  6. 算法分析。分析时间复杂性和空间复杂性,时间复杂性的影响因素为问题规模n、输入序列、算法本身;空间复杂性的影响因素为算法本身、输入输出数据、辅助变量;
  7. 算法的计算机实现和测试;
  8. 文档资料的编制。

1.5 时间复杂性和空间复杂性

1.5.1 时间复杂度

  1. 找到执行次数最多的代码(或运算)作为基本运算;
  2. 按基本运算的执行次数评估时间复杂度(按最坏情况)。

1.5.2 空间复杂度

1.5.3 渐进复杂性态

n逐渐增大的时候,时间复杂度是怎么增加的?

1.6 递归

  1. 全局变量动态分布变量(malloc、new)、静态局部变量在堆中分配;
  2. 子程序(或函数、方法)在被调用的那一刻,在系统数据栈的栈顶分配自己的局部变量和形式参数的存储空间;
  3. 子程序的局部变量和形式参数的存储空间在子程序返回前最后一刻从栈顶释放;
  4. 子程序只使用栈顶的那一份局部变量和形式参数;

1.6.1 递归算法的设计步骤

  1. 分析问题、寻找递归关系。找出大规模问题和小规模问题的关系,把规模大的问题转换成规模小的问题;
  2. 找出停止条件。该停止条件用来控制递归何时终止;
  3. 设计递归算法、确定参数,即构建递归体。

1.6.2 n的阶乘

#include "stdio.h"

int f(int n) {
	if (n == 0)
		return 1;
	else
		return n * f(n - 1);
}

int main() {
	int n;
	printf("请输入n的值:");
	scanf_s("%d", &n);
	int r = f(n);
	printf("%d的阶乘是:%d",n,r);
	return 0;
}

1.6.3 二叉树的遍历及结点求法

二叉树遍历的时候,内存使用了栈,要了解进栈出栈的过程。

#include "stdio.h"
#include "stdlib.h"

// 二叉树的存储结构
typedef struct BiTNode {
	char data;
	struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;

// 构建一棵二叉树
void CreateBiTree(BiTree* T) {
	char ch;
	scanf_s("%c", &ch);
	if (ch == '#')
		*T = NULL;
	else {
		*T = (BiTree)malloc(sizeof(BiTNode));	// 动态分配内存空间
		(*T)->data = ch;				// 生成结点
		CreateBiTree(&(*T)->lchild);	// 构造左子树
		CreateBiTree(&(*T)->rchild);	// 构造右子树
	}
}

void visit(BiTree T) {
	printf("%c ", T->data);
}

// 先序遍历
void PreOrder(BiTree T) {
	if (T != NULL) {
		visit(T);
		PreOrder(T->lchild);
		PreOrder(T->rchild);
	}
}

// 中序遍历
void InOrder(BiTree T) {
	if (T != NULL) {
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}

// 后序遍历
void PostOrder(BiTree T) {
	if (T != NULL) {
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}

int Count(BiTree T){
	if (T != NULL)
		return Count(T->lchild) + Count(T->rchild) + 1;
	else
		return 0;	//空二叉树为0结点
}

int main() {
	BiTree T = NULL;	// 构建一棵树 12#46###3#5##
	printf("请以先序遍历方式输入:");
	CreateBiTree(&T);
	printf("递归前序遍历结果:");
	PreOrder(T);
	printf("递归中序遍历结果:");
	InOrder(T);
	printf("递归后序遍历结果:");
	PostOrder(T);
	printf("二叉树结点总数为:%d", Count(T));
}

C/C++经典算法细解_第1张图片

二、会议安排算法【含冒泡排序】

2.1. 问题描述

  • 在有限的时间内有很多会议要召开,每个会议有开始和结束时间要求任何两个会议不能同时进行
  • 会议安排问题要求就是在所给的会议集合中选出最大的相容活动子集,即尽可能在有限的时间内召开更多的会议。

2.2 算法设计

每次从剩下未安排会议中选出最早结束且与已安排会议不冲突的会议【选最早结束时间】

#include "stdio.h"
#define n 10

struct TypeElem {
    //定义开始结束时间
    int Begin, End;
};
TypeElem A[n] = {
    {3,6},{1,4},{5,7},{2,5},{5,9},{3,8},{8,11},{6,10},{8,12},{12,14}
};

/*
	冒泡排序:
	从前往后依次比较两个元素的大小,左比右大则交换,否则不动
	第一趟排出最小的在第一位,然后再冒泡排序
	如果值相同则不用换位置
*/
void Sort(TypeElem A[], int k) {
    for (int i = 0; i < k; i++) {
        for (int j = 0; j < k-i-1; j++) {
            if (A[j].End > A[j+1].End) {
                TypeElem typeelem;
                typeelem = A[j];
                A[j] = A[j+1];
                A[j+1] = typeelem;
            }
        }
    }
};

int main() {
    //冒泡排序-会议最早结束时间的升序
    Sort(A,n);
    //排序后的结果展示
    for (int i = 0; i < n; i++) {
        printf("%d,%d",A[i].Begin,A[i].End);
        printf("\n");
    }
    printf("----------\n");
    //定义开始的时间
    int T = 0;
    //存储最终的会议
    TypeElem Meet[n];
    int m = 0;
    for (int j = 0; j < n; j++) {
        if (A[j].Begin >= T) {
            //安排此会议
            Meet[m] = A[j];
            T = A[j].End;
            m++;
        }
    }
    //安排会议后的结果展示
    for (int k = 0; k < m; k++) {
        printf("%d,%d", Meet[k].Begin, Meet[k].End);
        printf("\n");
    }
    return 0;
}

三、哈夫曼树和哈夫曼编码

3.1 算法简易描述

  1. 先找两个权值相加最小的结点,构造一棵新的小树;
  2. 再从其他的几个结点依次找出最小的组成树;
  3. 最后把所有结点都遍历完即可;
  4. 算法能看懂即可,不要求实现,要能够画出哈夫曼树和哈夫曼编码。
    C/C++经典算法细解_第2张图片

3.2 算法实现

8棵结点树:5,29,7,8,14,23,3,11

#include "stdio.h"
#define Maxnode 8

// 哈夫曼树的结点结构
struct HtNode{
	int weight;		// 权值
	int parent, lchild, rchild;		// parent:该结点的双亲结点在数组中的下标;lchild:左孩子在数组中的下标;rchild:右孩子在数组中的下标
};

// 哈夫曼树的存储结构
struct HtNode ht[2 * Maxnode - 1];

void Huffman(int n, int w[Maxnode]) {
	int i, j, p1, p2;	//p1,p2用来记录权值最小的两棵树在数组中的位置
	int min1, min2;		//min1,min2用来记录两个最小的权值
	if (n <= 1)
		return;
	// 初始化ht数组:前n个赋的值是叶子结点的值,从n+1开始到2n-1个是还未使用的结点赋值-1
	for (i = 0; i < 2 * n - 1; i++) {
		ht[i].parent = 0;
		if (i < n)
			ht[i].weight = w[i];
		else
			ht[i].weight = -1;
	}
	// 执行n-1次合并操作,即构造哈夫曼树的n-1个内部结点
	for (i = 0; i < n - 1; i++) {
		p1 = p2 = 0;
		min1 = min2 = 10000;
		// 从ht中选择权值最小的两个结点
		for (int j = 0; j < n + i; j++) {
			// 当此结点的父节点为0时表示未被选过
			if (ht[j].parent == 0) {
				// 查找权值最小的结点,用p1记录下标
				if (ht[j].weight < min1) {
					min2 = min1;	// 把min1赋值给min2
					min1 = ht[j].weight;	// 用min1记录这个权值
					p2 = p1;
					p1 = j;	// p1为最小权值结点的下标
				} else if(ht[j].weight < min2){
					min2 = ht[j].weight;
					p2 = j;	// p2为次小权值结点的下标
				}
			}
		}
		// 将ht[p1]和ht[p2]进行合并,其双亲是i
		ht[p1].parent = n + i;
		ht[p2].parent = n + i;
		ht[n + i].weight = min1 + min2;
		ht[n + i].lchild = p1;
		ht[n + i].rchild = p2;
	}
}

int main() {
	int w[Maxnode] = { 5,29,7,8,14,23,3,11 };
	Huffman(Maxnode, w);
	for (int i = 0; i < (2 * Maxnode - 1); i++) {
		printf("权值为:%d,左孩子为:%d,右孩子为:%d,父节点为:%d",ht[i].weight,ht[i].lchild,ht[i].rchild,ht[i].parent);
		printf("\n");
	}
	return 0;
}

3.3 哈夫曼编码

依据约定:左分支为0,右分支为1,获得哈夫曼编码即可。【左右子树为0或1根据题目而定,不唯一】

四、最小生成树

  1. 最小生成树不是唯一的,但其权值是一样的;
  2. 最小生成树的边数为顶点数减1;
  3. 若T为R中边的权值最小的生成树,则T称为G的最小生成树MST。

4.1 Prim算法

  1. 初始时任取一顶点加入树T,此时树中有1个顶点;
  2. 每次选择一个与当前T的顶点距离最短(边的权值最小)的顶点,将此顶点加到生成树中;
  3. 将所有顶点都加入树中结束。
struct edge{ double weight; int u,v; bool K; }; // 边。weight:权重; K为真:在生成树上
void Prim( edge Topology[ ], int m, int n){
	// Topology:原图的m条边,已按权重升序排好序;顶点编号从0开始,n个;
	int Point[n], num=0, i, j;
	for( Point[0]=0, num=1, m1=0; num<n; num++ ){
		for( i=0; i<m; i++ ){
			if( !Topology[i].K ){
				bool U,V; U=V= false;
				for( int j=0; j<num; j++ ){
 					if(Topology[i].u==Point[j]) U=true;
					if(Topology[i].v==Point[j]) V=true;
				}
				if( U && !V ) Point[num] = Topology[i].v;
				if( !U && V ) Point[num] = Topology[i].u;
				if( (U&&!V) || (!U&&V) ){
					Topology[i].K=true;
					break;
				}
			}
		}	
	}
}

C/C++经典算法细解_第3张图片

4.2 Kruskal算法

  1. 按权值递增顺序来选择合适的边来构造最小生成树;
  2. 每次选择一条权值最小的边,使这条边的两头连通(如果已经连通则不选);
  3. 所有边都入树后结束。
struct edge{ double weight; int u,v; bool K; }; // 边。weight:权重; K为真:在生成树上
struct  TypeTree { int *A; int len; };
void Merg(TypeTree &P, TypeTree &Q ){// 合并P,Q两棵树到P
	int *M;  M = new int[ P.len+Q.len ]for( int i=0; i<P.len; i++) M[i] = P.A[i];
	for( int i=0; i<Q.len; i++) M[i+P.len] = Q.A[i];
	delete [ ]Q.A; Q.A=NULL; Q.len=0;
	delete [ ]P.A;  P.A=M;  P.len+=Q.len
}
void Kruskal( edge Topology[ ], int m, int n){
	// Topology:原图的m条边,已按权重升序排好序;顶点编号从0开始,n个;
	TypeTree Tree[n];    int t, e, q;
	for( int i=0; i<n; i++ ){ Tree[i].A = new int[1]; Tree.len=1; Tree.A[0]=i; }
	for( int nTree = n; nTree>1; nTree-- ){
		for( e=0; e<m; e++ ){
			if( Topology[ e ].K ) continue;
			int T1= -1, T2= -1;
			for( t=0; t<n; t++ ){
				for( q=0; q<Tree[t].len; q++){
 					if(Topology[ e ].u==Tree[ t ].A[ q ] ) T1 = t;
					if(Topology[ e ].v==Tree[ t ].A[ q ] ) T2 = t;
				}
			}
			if( T1 != T2 ){
				Topology[ e ].K = true;
				Merg(Tree[T1], Tree[T2]);
				break;
			}
		}
	}
}

C/C++经典算法细解_第4张图片

五、快速排序

5.1 算法简易描述

  1. 选取一个基准元素,一般选第一个数;
  2. 从最后一个数开始比较,比基准元素大则不动,指针左移即R- -;
  3. 直到比较出一个比基准元素小的数,则把这个数放到基准元素的位置,这个位置空出来;
  4. 从基准元素向右移,即L++,与基准元素比较,若大则继续换位置;
  5. 最后两指针重合,则将基准元素放到这个位置。
  6. 循环出来时候两指针重叠,则说明左右指针都指向中间的基准元素的位置,此时将基准元素赋值即可;
  7. 快速排序的平均时间复杂度为O(nlogn).

5.2 算法实现

#include "stdio.h"
#define length 8

int QuickPass(int A[], int L, int R) {
	int T = A[L];	// 基准元素为第一个元素
	while (L < R) {
		// 从最后一个元素开始比较
		while (L < R && A[R] >= T) R--;	// 循环比较,比基准元素大,则保持不动,指针左移
		A[L] = A[R];
		while (L < R && A[L] <= T) L++;
		A[R] = A[L];
	}
	// 最后两个指针重合,则将基准元素给这个空位
	A[L] = T;
	return L;
}

void QuickSort(int A[], int L, int R) {
	if (L >= R) return;
	int M = QuickPass(A, L, R);
	QuickSort(A, L, M-1);
	QuickSort(A, M+1, R);
}

int main() {
	int A[length] = {49,38,65,97,76,13,27,49};
	QuickSort(A, 0, length-1);
	//遍历输出结果 
	for (int i = 0; i < length; i++) {
		printf("%d  ", A[i]);
	}
	return 0;
}

六、二分查找

6.1 算法简易描述

  1. 将头设为low,尾设为high,中间位置为middle
  2. 二分查找的前提是已经排好序的数,故要找的数Key和middle比,大则操作low,小则操作high,找到找不到循环结束之后再判断;
  3. if语句里的情况分为4种,其中2,3种会出现找不到元素出错的问题,请注意。

6.2 算法实现

#include "stdio.h"
#include "string"
using namespace std;

struct TypeData {
	string Name;
	int Age;
	// 构造函数
	TypeData(string Name, int Age) :Name(Name), Age(Age) {}
	TypeData():Age(0){}
};

const int Size = 10;
// 定义线性表
struct TypeList {
	// 数组
	TypeData Array[Size];
	// 有效元素个数
	int n;
};

int Binary_Search(TypeList List, string Key) {
	// low是首位置,high是末位置,middle是中间位置
	int low = 0, high = List.n - 1, middle;
	while (low < high) {
		middle = (low + high) / 2;
		/*
		* if语句的条件里,
			1.	if(Key <= List.Array[middle].Name) high = middle;
				else low = middle+1;
			2.	if(Key < List.Array[middle].Name) high = middle;
				else low = middle
			3.  if(Key >= List.Array[middle].Name) low = middle;
				else high = middle;
			4.	if(Key > List.Array[middle].Name) low = middle+1;
				else high = middle;
		*/
		if (Key >= List.Array[middle].Name) {
			low = middle;
		} else {
			high = middle;
		}
		if (Key == List.Array[low].Name) {
			return low;
		}
	}
	return -1;
}

int main() {
	TypeList List;
	List.Array[0] = TypeData("A1",111);
	List.Array[1] = TypeData("B1",123);
	List.Array[2] = TypeData("C1",456);
	List.Array[3] = TypeData("D1",789);
	List.Array[4] = TypeData("E1",110);
	List.n = 5;
	string K = "B1";
	int result = Binary_Search(List, K);
	if (result == -1)
		printf("未找到!");
	else
		printf("找到!");
	return 0;
}

七、深度优先搜索

7.1 算法简易描述

  1. 深度优先搜索类似于树的先序遍历,跟树的深度优先遍历相比,图的深度优先遍历多了一个一维数组的数据结构
  2. 从一个顶点出发,先访问这个顶点,再递归访问这个顶点所连接的另一个顶点;
  3. 假设从2开始遍历,下面图的遍历顺序为:21563478
    C/C++经典算法细解_第5张图片

7.2 算法实现

  • 本算法使用到的数据结构:栈、邻接矩阵、一维数组
  • 仅需看懂中间递归调用代码即可
#include "stdio.h"
#include "stdlib.h"
#define MAX_VERTEX_NUM	 30

// 一维数组:记录顶点是否被访问
bool visited[MAX_VERTEX_NUM];

typedef struct AMGraph {
	int vexs[MAX_VERTEX_NUM];	// 顶点表
	int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];	// 邻接矩阵
	int vexnum, arcnum;		// 当前的顶点数和边数
}AmGraph;


// 找到顶点v的对应下标
int LocationVex(AMGraph& G, int v) {
	for (int i = 0; i < G.vexnum; i++)
		if (G.vexs[i] == v)
			return i;
}

// 用邻接矩阵表示法,创建无向图G
int CreateG(AMGraph& G) {
	int i, j, k;
	G.vexnum = 8;	// 总顶点数为8
	G.arcnum = 10;	// 总边数为10
	for (i = 0; i < G.vexnum; i++)
		G.vexs[i] = i + 1;	// 顶点的信息
	for (i = 0; i < G.vexnum; i++) {
		for (j = 0; j < G.vexnum; j++)
			G.arcs[i][j] = 0;	// 初始化邻接矩阵的边,0表示顶点i和j之间无边
	}
	int A[10][2] = { {1,2},{1,5},{2,6},{3,6},{3,7},{3,4},{4,7},{4,8},{6,7},{7,8} };
	for (k = 0; k < G.arcnum; k++) {
		i = LocationVex(G, A[k][0]);
		j = LocationVex(G, A[k][1]);
		G.arcs[i][j] = G.arcs[j][i] = 1;	// 1表示顶点i和j之间有边,无向图不区分方向,对称矩阵
	}
	return 1;
}

void DFSV0(AMGraph& G, int v) {
	printf("%d ", G.vexs[v]);
	visited[v] = true;		// 访问过之后置true
	for (int i = 0; i < G.vexnum; i++)
		if (G.arcs[v][i] && !visited[i]) //递归调用
			DFSV0(G, i);
}

void DFS(AMGraph G) {
	for (int i = 0; i < MAX_VERTEX_NUM; i++)
		visited[i] = false;	// 初始化顶点数组,false表示未访问
	for (int i = 1; i <= G.vexnum; i++) {	// 从0号顶点开始遍历
		if (!visited[i])
			DFSV0(G, i);
	}
}

int main() {
	AMGraph G;
	int c = CreateG(G);	// 创建无向图
	if (c)
		DFS(G);
	else
		printf("创建无向图失败!");
	return 0;
}

C/C++经典算法细解_第6张图片

八、宽度优先搜索

8.1 算法简易描述

  1. 类似于二叉树的层次遍历,一层一层地遍历每个结点;
  2. 先访问起始顶点v0,再由v0出发,访问v0的未访问过的邻接顶点w1…,然后继续访问w1…的邻接顶点;
  3. 直到所有顶点都遍历完,则结束。

【注】

  • 进过队列的不能再进队列,当一个顶点进队列的时候打标记;
  • 循环开始的条件:把第一个顶点打好标记然后加到队列里;
  • 循环结束的条件:队列空的时候结束,一幅图的一个连通子图搜索完毕。

8.2 算法实现

  • 本例采用了的数据结构:队列、邻接矩阵
#include "stdio.h"
#include "queue"
#define MAX_VERTEX_NUM 8	// 最大顶点数
#define _CRT_SECURE_NO_WARNINGS
using namespace std;

queue<int> q;	// 定义一个队列,使用库函数queue
bool visited[MAX_VERTEX_NUM];	// 定义一个数组,记录已被访问的顶点

typedef struct AMGraph {
	int vexs[MAX_VERTEX_NUM];	// 顶点表
	int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];	// 邻接矩阵
	int vexnum, arcnum;		// 当前的顶点数和边数
}AmGraph;


// 找到顶点v的对应下标
int LocationVex(AMGraph& G, int v){
	int i;
	for (i = 0; i < G.vexnum; i++)
		if (G.vexs[i] == v)
			return i;
}

// 用邻接矩阵表示法,创建无向图G
int CreatG(AMGraph& G) {
	int i, j, k;
	G.vexnum = 8;	// 总顶点数为8
	G.arcnum = 10;	// 总边数为10
	for (i = 0; i < G.vexnum; i++) 
		G.vexs[i] = i + 1;	// 顶点的信息
	for (i = 0; i < G.vexnum; i++) {
		for (j = 0; j < G.vexnum; j++)
			G.arcs[i][j] = 0;	// 初始化邻接矩阵的边,0表示顶点i和j之间无边
	}
	int A[10][2] = { {1,2},{1,5},{2,6},{3,6},{3,7},{3,4},{4,7},{4,8},{6,7},{7,8} };
	for (k = 0; k < G.arcnum; k++) {
		i = LocationVex(G, A[k][0]);
		j = LocationVex(G, A[k][1]);
		G.arcs[i][j] = G.arcs[j][i] = 1;	// 1表示顶点i和j之间有边,无向图不区分方向,对称矩阵
	}
	return 1;
}

// 宽度优先遍历(从v0开始遍历)
void BFSV0(AMGraph& G, int v0){
	int i, v, head, w;
	v = LocationVex(G, v0);
	visited[v] = true;		// true表示顶点v已被访问
	q.push(v0);			//顶点v0入队列
	printf("%d",v0);
	while (!q.empty()) {
		head = q.front();			// 取队头元素head
		v = LocationVex(G, head);	// 得到顶点head的下标
		q.pop();					// 顶点head出队
		for (i = 0; i < G.vexnum; i++) {
			w = G.vexs[i];			// 遍历记录每个顶点,看和w之间有没有边
			if (G.arcs[v][i] && !visited[i]) {	// 如果顶点head和w之间有边,并且顶点未访问
				visited[i] = true;	// 将w的位置标记为true
				q.push(w);	// 将w入队
				printf("%d", w);
			}
		}
	}
}

void BFS(AMGraph G) {
	for (int i = 0; i < MAX_VERTEX_NUM; i++)
		visited[i] = false;	// 初始化顶点数组,false表示未访问
	for (int i = 1; i <= G.vexnum; i++) {	// 从0号顶点开始遍历
		if (!visited[i])
			BFSV0(G, i);
	}
}

int main() {
	AMGraph G;
	int c = CreatG(G);	// 创建无向图
	if (c)
		BFS(G);
	else
		printf("创建无向图失败!");
	return 0;
}

九、01背包问题

9.1 问题描述

将n个物品【分别价值为v0,v1…】装入容量为W的背包中,问怎样才能实现装入的价值最大。

9.2 算法实现

#include "stdio.h"
#define n 5

struct TypeE {
	int W;	// 物品重量
	int V;	// 物品价值
};

TypeE A[n] = { {2,3}, {3,4}, {4,5}, {5,6}, {1,1} };
int BagW = 8;
TypeE R = { 0, 0 };
int V0 = 0;

// 返回方案S的总重量,引用V里是总价值
int WV(unsigned long S, int& V) {
	int W = 0;
	V = 0;
	unsigned long K = 0x01;	// K为逻辑尺
	for (int i = 0; i < n; i++) {
		if (K & S) {
			W += A[i].W;
			V += A[i].V;
		}
		K = K << 1;
	}
	return W;
}

int main() {
	int Result;
	for (int i = 0; i < 31; i++) {
		int V = 0;	// 初始化每次循环物品的价值为0
		if (i != 0) {
			int res_w = WV(i, V);
			if (res_w <= BagW && R.V >= V0) {
				R.W = res_w;
				R.V = V;
				V0 = R.V;
				Result = i;
			}
			
		}
	}
	for (int i = 0; i < n; i++) {
		if (Result & (1 << i))
			printf("挑选第%d个物品,即[%d,%d] \n", i+1, A[i].W, A[i].V);
	}
	printf("总重量是:%d, 总价格为%d", R.W, R.V);
	return 0;
}

十、布线问题

10.1 算法简易描述

  1. 从a到b的路线有很多条,求一条最短的路径;
  2. 用到的数据结构为队列;
  3. 进过队列的不能再进,进队列的数据打标记;
  4. 初始化:把起点行列下标加到队列;
  5. 结束:找到终点【有可能走不到终点,此时特点是队列为空,报告未走通】。

10.2 算法实现

#include "stdio.h"
#include "queue"
using namespace std;

struct Position {
	int row, col;
};
int grid[11][9];	// 表盘数组

void showPath(){
	printf("----------------------输出列阵开始----------------------\n");
	for (int i = 0; i < 11; i++){
		for (int j = 0; j < 9; j++)
			printf("%d\t",grid[i][j]);
		printf("\n");
	}
	printf("----------------------输出列阵结束----------------------\n");
}

void initGrid() {
	// 1. 上下左右边框均设置为-2
	for (int i = 0; i < 9; i++)
		grid[0][i] = grid[10][i] = -2;
	for (int i = 1; i < 10; i++)
		grid[i][0] = grid[i][8] = -2;
	// 2. 除边框外其他部分赋值为-1
	for (int i = 1; i <= 9; i++) {
		for (int j = 1; j <= 7; j++) {
			grid[i][j] = -1;
		}
	}
	// 3. 障碍点设置为-2
	grid[1][5] = grid[9][3] = grid[9][5] = -2;
	for (int i = 2; i <= 4; i++)	grid[i][3] = -2;
	for (int i = 5; i <= 6; i++)	grid[3][i] = -2;
	for (int i = 5; i <= 7; i++)	grid[i][6] = -2;
	for (int i = 4; i <= 5; i++)	grid[6][i] = -2;
}

bool findPath(Position start, Position finish, int &PathLen, Position* &path) {
	// 判断起点和终点是否重合
	if (start.col == finish.col && start.row == finish.row) {
		PathLen = 0;
		return true;
	}
	Position offset[4];	// 定义四个方向
	offset[0].row = 0;	offset[0].col = 1;	//右
	offset[1].row = 1;	offset[1].col = 0;	//下
	offset[2].row = 0;	offset[2].col = -1;	//左
	offset[3].row = -1;	offset[3].col = 0;	//上
	Position here, nbr;		// 当前结点、相邻结点
	here.row = start.row;	// 起初的当前结点为起点
	here.col = start.col;
	grid[here.row][here.col] = 0;	// 当前结点值为0
	queue<Position> Q;		// 定义一个队列
	do {
		for (int i = 0; i < 4; i++) {
			nbr.row = here.row + offset[i].row;
			nbr.col = here.col + offset[i].col;
			if (grid[nbr.row][nbr.col] == -1) 		// 该方格未标记
				grid[nbr.row][nbr.col] = grid[here.row][here.col] + 1;
			if (nbr.row == finish.row && nbr.col == finish.col)	// 找到
				break;
			if (grid[nbr.row][nbr.col] == -2)
				continue;
			Q.push(nbr);
		}
		if (nbr.row == finish.row && nbr.col == finish.col)
			break;	// 布线成功,跳出外层循环
		if (Q.empty())
			return false;	// 无解
		here = Q.front();
		Q.pop();	// 取下一个扩展结点
	} while (true);
	// 逆向构造最短路线
	PathLen = grid[finish.row][finish.col];
	path = new Position[PathLen];
	here = finish;
	for (int j = PathLen - 1; j >= 0; j--) {
		path[j] = here;
		// 寻找前驱
		for (int k = 0; k < 4; k++) {
			nbr.row = here.row + offset[k].row;
			nbr.col = here.col + offset[k].col;
			if (grid[nbr.row][nbr.col] == j)
				break;
		}
		here = nbr;	// 向前移动
	}
	return true;
}

int main() {
	Position start;		// 定义起点
	start.col = 2;
	start.row = 3;
	Position finish;	// 定义终点
	finish.col = 7;
	finish.row = 6;
	initGrid();			// 初始化表盘
	showPath();
	int PathLen = 0;
	Position* path;
	bool flag = findPath(start, finish, PathLen, path);	// 开始寻找最短布线路径,找到返回true,未找到返回false
	if(flag){
		showPath();
		printf("最短路径经过:\n");
		for (int i = 0; i <= PathLen; i++)
			printf("第%2d个单元格的坐标为:第%d行, 第%d列\n", i + 1, path[i].row, path[i].col);
	}else
		printf("布线失败");
	return 0;
}

十一、随机化算法

11.1 伪随机数发生器

  • 现实生活中无法产生真正的随机数,因此在随机化算法里使用的随机数都是在一定程度上的随机,即“伪随机数”
  • 通过一个固定的、可以重复的计算方法产生的发生器叫做伪随机数发生器
  • 通常,计算机中产生伪随机数的方法是线性同余法,产生的随机数序列为:a0,a1,…,an满足:在这里插入图片描述
  • d为种子,b为系数(b≥0),c为增量(c≥0),m为模数(m>0)
  • b,c,m越大且b与m互质可使随机性能变得更好。当b,c,m确定后给定一个种子d,由上式产生的随机序列也就确定了
  • mod m的意思是,超过m,就把这个数减去m。打个比方,x mod 12 代表x的值永远在0…11之间,这种方法叫做“同余”

11.2 随机化计算圆周率

  • 利用圆与其外切正方形的面积之比来计算π的近似值
  • 将n个点随机投向一个正方形,设落入正方形内切圆中的点的数目为k,则所投入的点落入圆内的概率为在这里插入图片描述
  • 当n趋近于无穷大的时候,k/n = Π/4,从而Π≈4k/n
#include "stdio.h"
#include "time.h"
#include 
#define m 65536L
#define b 1194211693L
#define c 12345L
using namespace std;

class RandomNumber{
public:
    RandomNumber(unsigned long s = 0);  // 默认值0表示由系统自动产生种子
    unsigned short random(unsigned long n);     // 产生0:n-1之间的随机整数
    double fRandom(void);   // 产生[0,1)之间的随机实数
private:
    unsigned long d;    // d为当前种子
};

// RandomNumber用来生产种子
RandomNumber::RandomNumber(unsigned long s){
    if (s == 0)
        d = time(0);    // 由系统时间产生种子
    else
        d = s;
}

// random用来产生0:n-1之间的随机整数
unsigned short RandomNumber::random(unsigned long n) {
    d = b * d + c;      //用线性同余式计算新的种子d
    return (unsigned short)((d >> 16 % n)); // 把d的高16位映射到0~(n-1)的范围内
}

// fRandom用来产生[0,1)之间的随机实数
double RandomNumber::fRandom(void) {
    return random(m) / double(m);
}

// 用随机投点法计算 PI
double Darts(int n){
    static RandomNumber darts;
    int k = 0;
    for (int i = 1; i <= n; i++) {
        double x = darts.fRandom(); // 产生一个[0,1)之间的实数
        double y = darts.fRandom();
        if ((x * x + y * y) <= 1) {
            k++;
        }
    }
    return (4 * k) / (double)n;
};

int main(){
    cout << Darts(200000000) << endl;
    return 0;
};

11.3 随机化法计算定积分

  • 随机投点法计算定积分。假设向单位正方形里投入n个点,如果有m个点落入积分区域G内,则积分I近似等于随机点落入G内的概率,即I=m/n
#include "stdio.h"
#include "time.h"
#include 
#define m 65536L
#define b 1194211693L
#define c 12345L
using namespace std;

class RandomNumber {
public:
    RandomNumber(unsigned long s = 0);  // 默认值0表示由系统自动产生种子
    unsigned short random(unsigned long n);     // 产生0:n-1之间的随机整数
    double fRandom(void);   // 产生[0,1)之间的随机实数
private:
    unsigned long d;    // d为当前种子
};

// RandomNumber用来生产种子
RandomNumber::RandomNumber(unsigned long s) {
    if (s == 0)
        d = time(0);    // 由系统时间产生种子
    else
        d = s;
}

// random用来产生0:n-1之间的随机整数
unsigned short RandomNumber::random(unsigned long n) {
    d = b * d + c;      //用线性同余式计算新的种子d
    return (unsigned short)((d >> 16 % n)); // 把d的高16位映射到0~(n-1)的范围内
}

// fRandom用来产生[0,1)之间的随机实数
double RandomNumber::fRandom(void) {
    return random(m) / double(m);
}

// 用随机投点法计算 PI
double Darts(int n) {
    static RandomNumber darts;
    int k = 0;
    for (int i = 1; i <= n; i++) {
        double x = darts.fRandom(); // 产生一个[0,1)之间的实数
        double y = darts.fRandom();
        if ((x * x + y * y) <= 1) {
            k++;
        }
    }
    return k / (double)n;
};

int main() {
    cout << Darts(200000000) << endl;
    return 0;
};

十二、NP完全理论

12.1 易解问题和难解问题

  • 将存在多项式时间算法的问题叫做易解问题。例如:排序问题、查找问题、欧拉回路
  • 将需要指数时间算法解决的问题叫做难解问题。例如:TSP问题、Hanio问题、Hamilton回路

12.2 P类问题

  • 这里P指代Polynomial。P问题是指能够在多项式时间内解决的问题,这意味着计算机能够在有限时间内完成计算
  • P问题可以用多项式时间的确定性算法来进行判定或求解
  • 所有易解问题都是P类问题。例如最短路径判断问题

12.3 NP类问题

  • 所有的非确定性多项式时间可解的判定问题构成NP类问题
  • NP类问题可以用多项式时间的非确定性算法来进行判定或求解

12.4 NP完全问题

  • NP完全问题是NP类问题的一个子类,是更为复杂的问题
  • 如果一个NP完全问题能在多项式时间内得到解决,那么NP问类中的每一个问题都可以在多项式时间内解决,即P = NP成立
    C/C++经典算法细解_第7张图片

12.5 NP完全问题的近似算法

近似解:用广度优先算法,用分支界限法

注意:本博客仅提供算法参考,有问题的地方欢迎指正。

你可能感兴趣的:(笔记,算法设计,算法)