第7章 暴力求解法的读书笔记

简单枚举

7.1.1 除法 ——并没有搞出来,不知道出了什么问题

7.1.2 乘积 ——对于数据量小于18的这种小数据只求乘积最大的连续子序列的积是很好搞的,直接枚举序列的头指针和尾指针就可以了

                        但是如果是求和还求最小头尾指针,就是可以用更好的办法,只循环一遍:

                             flag = 1; neg是头指针;sum是最终值,初始值0

	for( i = 0; i < k; i++)
	{
		if( a[i] < 0)neg++;
		thissum = thissum + a[i];
		 if( thissum > sum)       // 大于sum则赋值
		{
			sum = thissum;
			end = i;
			if( flag == 1)
			{
				temp = i; // 是否切换头指针

				flag = 0;
			}
		}
		else if( thissum < 0)     // 小于0直接舍弃
		{
			thissum = 0;
			flag = 1;         // 是否切换头指针
		}	
	}

7.1.3 分数拆分 ——直接枚举或变形枚举都可,主要是判断枚举边界条件,其中一个x一定小于2k

7.1.4 双基回文数 ——对于每一个数以及每一个可能的基数都暴力尝试,小于10的6次方似乎是一个很小的数据规模

枚举排列

7.2.1 生成1~n的排列 ——依次每一位都从大到小进行枚举, 并且还要判断是否使用过,一般使用递归

7.2.2 生成可重集的排列 ——问题改编为:输入数组P,输出P中的元素全排列

                           思路是将P中的元素从小到大排序,然后枚举,判断是否曾经出现过,然后递归即可

                 几个需要解决的问题:

                          (1)P中的元素存在重复

                                   解决办法:对P中的重复元素进行计数

                          (2)在存在重复的情况下,可能会同一情况计算多次

                                   解决办法:枚举的下标 i,应该是不重复、不遗漏地取遍所有 P[i] 值,由于P数组已经排过序,所以只需检查P的第一个元素和所有“与前一个元素不相同”的元素,即应该加上 !i || P[i] != P[i-1] 的判断条件

#include<stdio.h>
#include<stdlib.h>
int cmp( const void* a, const void* b){
	return *(int*)a - *(int*)b;
}
int a[100] = {0}, vis[100] = {0}, p[100] = {0};
int n;
void permutation( int k);
int main(void){
	int i;
	scanf("%d", &n);
	for( i = 0; i < n; i++){
		scanf("%d", &p[i]);
	}
	qsort( p, n, sizeof(int), cmp);			       // 排序保证字典序
	permutation(0);
	return 0;
}
void permutation( int k){
	int i, j, c1, c2, b;
	if( k == n){
		for( i = 0; i < n; i++){
			printf("%d ", a[i]);
		}
		printf("\n");
	}
	else{
		for( j = 0; j < n; j++){
			c1 = c2 = 0;
			if( !j || p[j] != p[j-1]){                // 为第一个或不等于前一个
				for( b = 0; b < k; b++)          // 这里想当然了,一开始写成 b < n了,怎么都不对,之后才检查出来
				{
					if( a[b] == p[j]) c1++; // a数组中出现次数
				}
				for( b = 0; b < n; b++){
					if( p[b] == p[j]) c2++; // P数组中出现次数
				}
				if( c1 < c2){
					a[k] = p[j];
					permutation( k+1 );
				}
			}
		}
	}
}

7.2.3 解答树 ——显示了递归函数的调用过程,每一个叶子结点对应一个排列,这种树表示的是逐步产生完整结的过程,因此称为解答树。

                     如果某个问题的解可以由多个步骤得到,每个步骤都有若干种选择,且可以用递归方法实现,则工作方式可以用解答树描述。

7.2.4 下一个排列 ——枚举所有排列的方法是按照字典序最小的排列开始不停调用“求下一个排列的过程”,那么任意给一个排列P,如何寻找字典序内它的下一个序列呢?这个问题之前曾经查过,这里再复习一遍:

                     具体有4个步骤:

                              (1)从右往左寻找第一个位置 j ,满足 P[j] < P[j - 1];

                              (2)从右至左寻找 j 前比 P[j] 大的最小数,P[i];

                              (3)交换 i , j 位置的数;

                              (4)将 j 后的数从小到大的顺序进行排列;

                     应该也适用于有重集的序列

子集生成 ——给定一个集合,枚举它所有可能的子集

7.3.1 增量构造法 ——一次选出一个元素放到集合中。这里规定集合中的元素从小到大排列,避免了同一集合,不同顺序的输出

#include<stdio.h>					// 所求的是0~n的集合中的所有子集
int b[100];					// 可以放到函数中传递,只不过这么定义可以在调试时查看数组元素
void subset( int n, int cur){
	int i, s;
	for( i = 0; i < cur; i++)printf("%d ", b[i]); // 输出当前集合{其实这里可以加一句使程序可以输出空集}
	printf("\n");
	s = cur ? b[cur-1] + 1 : 0;			// 确定当前元素的最小可能值,当前集合的cur为0时最小元素为0,否则为上一个元素增1(因为是						    原集合连续的数)
	for( i = s; i < n; i++){			// 这里递归边界不需要显式规定,不需要添加元素时循环结束,递归也就结束了
		b[cur] = i;			// 将当前元素的值从最小到最大进行遍历
		subset( n, cur+1);			// 递归求下一个元素的值
	}
}
int main(){
	subset( 3, 0);
	return 0;
}

7.3.2 位向量法 ——构造一个位向量B,而不是直接构造子集A。当 i 在子集A中时,B[i] = 1。

#include<stdio.h>
int b[100] = {0};
int a[10] = { 0, 1, 2, 3, 4, 5};
void subset( int n, int cur){
	int i;
	if( cur == n){
		for( i = 0; i < cur; i++){
			if( b[i]) printf("%d ", a[i]);
		}
		printf("\n");
		return;			// 显然这里的边界需要显式规定
	}
	b[cur] = 1;			// 当前元素在集合中
	subset( n, cur + 1);
	b[cur] = 0;			// 当前元素不在集合中
	subset( n, cur + 1);
}
int main(){
	subset( 6, 0);
	return 0;
}

                   增量构造法的解答树中,每一个A都对应一个结点,而位向量法的解答树必须当所有元素都被遍历以后if( cur == n)才能进行一次输出,只有叶结点才是A的解答,叶结点以上的结点都是不完整解(部分解)。所以若前一种解答树为n层,则后一种为n+1层

7.3.3 二进制法 ——用二进制表示子集,多么强大的位运算

#include<stdio.h>
void print_subset( int n, int s){
	int i;
	for( i = 0; i < n; i++){
		if( s & (1 << i)) printf("%d ", i); // 1<<i表示的是将1左移i位,相当于扫描带着集合元素信息的s,当s的i位为1时说明i在集合中。						   不关心到底按位与后是什么数,利用非0值都为真的性质即可
	}
	printf("\n");
}
int main(){
	int i;
	for( i = 0; i < ( 1 << 3); i++){		// 原集合中有3个元素,一共有2的3次方种可能性(一般把全集定义为(1>>n)-1)
		print_subset( 3, i);		// 打印这种可能
	}
	return 0;
}

回溯法 ——递归构造中生成检查的过程结合起来

7.4.1 八皇后问题 ——对行列进行全排列的问题

                    引出回溯的定义:本问题分成若干的步骤并递归求解时,若当前步数没有合法选择,则返回上一级调用的现象

void search( int cur){					// 用一维数组实现 逐行进行尝试
	if( cur == 8){
		输出当前排列; 
		num++; 					// 算作一种可能 
	}
	else{
		for( int i = 0; i < 8; i++){		// 每行的每一列进行尝试
			queen[cur] = i; 			// 尝试把皇后放在这里 
			对皇后的位置进行检查:列,对角线; 
			if( 位置合法) search( cur + 1); 
		} 
	} 
}
void search( int cur){					// 用全局二维数组vis[][]辅助实现
	if( cur == 8){
		输出当前排列; 
 		num++; 					// 算作一种可能 
	}
	else{
		for( int i = 0; i < 8; i++){
			用二维数组vis[][]直接模拟判断,若合法{
				修改全局变量;
				search( cur + 1);
				将全局变量修改回来; 		// 一定要把全局变量改回来,若函数有多个出口,每个出口都要恢复原来的值
			} 
		} 
	} 
}

7.4.2 素数环 ——全排列运行很慢,回溯法有效提高速度

7.4.3 困难的串 ——逐个位置考虑字母,主要是如何检验后缀

#include<stdio.h>
int n, l, i, j, k, equal, ok;
int s[100];
int dfs( int cur){ 
	int i; 
	if( cur == n){				// 将序列输出
		for( i = 0; i < cur; i++)
			printf("%c ", 'A' + s[i]);
		printf("\n");
		return 0;				// 返回表示成功
	}
	else{
		for( i = 0; i < l; i++){		// 逐个赋值
			s[cur] = i;
			ok = 1;
			for( j = 1; j * 2 <= cur + 1; j++){			// 调整所检验的后缀长度为 j*2
				equal = 1;
				for( k = 0; k < j; k++)
					if( s[cur-k] != s[cur-k-j])		// 逐个检验后一半是否等于前一半										{ equal = 0; break;}
				if( equal){ ok = 0;break;}
			}
			if( ok)
				if( !dfs(cur+1)) return 0;			// 递归搜索,返回表示成功,若已经找到解,直接退出
		}
		return 1;							// 返回表示失败
	}
}
int main(){
	
	scanf("%d%d", &n, &l);
	dfs(0);
	return 0;
}

7.4.4 带宽 —— 和图有关,并没有静下心来去看图,就这么暂时跳过了

隐式图搜索 ——一些程序动态生成的图

7..5.1 隐式树的遍历 ——回溯法是深度优先顺序遍历的,优点是节省递空间:只有递归栈中的结点需要保存,也就是空间开销和深度成正比;而宽度(广度)优先遍历的优点 则是找到的第一个解一定是离根最近的解,但是结点队列中的结点可能会很多,空间开销很大。这个问题并没有查到比较好的答案,嗯

7.5.2 埃及分数 ——深搜没有明显的上界,宽搜一层也没有明显的界限,所以引入了迭代加深搜索。

                                             迭代加深搜索:对深度优先搜索进行限制,给出一个限制深度dmax,若深度到达dmax还没有找到答案,就放弃这个分支

【此外的图和哈希表STL没有看】TBC


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值