简单枚举
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