第二章:递归与分治策略
计算机中问题规模越小,越好求解,自然而然想到可不可以将大问题分割为小问题,分治思想由此诞生。
分治法的设计思想是:将一个难以直接解决的大问题分割成一些规模较小的相同问题,以便各个击破,即分而治之。
如果原问题可分割成k个子问题,\(1<k≤n\),且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。
由分治法产生的子问题往往是原问题的较小模式,这为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模不断缩小,最终使子问题缩小到容易求出其解,由此自然引出递归算法。
分治与递归像一对孪生兄弟,经常同时应用在算法设计中,并由此产生许多高效算法。
递归
直接或间接地调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数
1)n阶螺旋矩阵问题
问题描述:
螺旋矩阵是指一个呈螺旋状的矩阵,它的数字由第一行开始到右边不断变大,向下变大,向左变大,向上变大,如此循环。
创建n阶螺旋矩阵并输出。
输入描述:
输入包含多个测试用例,每个测试用例为一行,包含一个正整数n(1≤n≤50),用输入0表示结束。
输出描述:
每个测试用例输出n行,每行包含n个整数,整数之间用一个空格分隔。
样例
输入:
4
0
输出:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
解析
我们发现,实际上,每一个大的螺旋矩阵里面都是包含着一个小的螺旋矩阵的!那么递归结构不就出来了吗,我们要解决一个大的螺旋矩阵问题,只用把它四周的“边框”填充起来,然后内部实际上就成了一个规模小一点的螺旋矩阵问题,递归解决就可以了!
经过分析所以我们的递归式大概是:f (大规模) = 填充四个边框 + f (小规模)
至于是什么参数,那就由你来设了。我设了四个参数:
1.螺旋矩阵起始数字s
2.螺旋矩阵终止数字e(其实这个不需要)
3.螺旋矩阵的长度len(要用来做边界判断的)
4.表示当前是第几个螺旋矩阵,数字k
————————————————
版权声明:本文为CSDN博主「AAS48」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_40163242/article/details/88025947
代码
#include<iostream>
using namespace std;
const int maxn = 60;
int n;
int a[maxn][maxn];
void f(int s, int e, int len, int k);
int main()
{
cin >> n;
f(1, n * n, n, 1); //从k = 1开始,第一个矩阵
//print a
for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= n;j++)
{
printf("%5d", a[i][j]); //输出右对齐
}
cout << endl;
}
return 0;
}
//len表示矩阵的边长,也属于一个问题参量 , k表示当前是第几个矩阵
void f(int s, int e, int len, int k) //将i---j的螺旋矩阵装到二维数组中
{
//递归边界
if(len == 1)
{
a[k][k] = s;
return ;
}
else if(len == 2)
{
a[k][k] = s++;
a[k][k + 1] = s++;
a[k + 1][k + 1] = s;
a[k + 1][k] = e;
return ;
}
//矩阵上边框
int col = n + 1 - k; //表示右边框的列号和下边框的行号
int x = s;
for(int j = k;j <= col;j++) //这里循环条件之前手误写成j <= len了,导致错误
{
a[k][j] = x;
x++;
}
//矩阵右边框
for(int i = k + 1;i <= col;i++)
{
a[i][col] = x;
x++;
}
//矩阵下边框
for(int j = col - 1;j >= k;j--)
{
a[col][j] = x;
x++;
}
//矩阵左边框
for(int i = col - 1;i >= k + 1;i--)
{
a[i][k] = x;
x++;
}
f(x, n * n, len - 2, k + 1); //递归,填充小螺旋矩阵
}
2)求解幸运数问题
问题描述:
小明同学学习了不同的进制之后,拿起了一些数字做起了游戏。小明同学知道,在日常生活中我们最常用的是十进制数,而在计算机中,二进制数也很常用。现在对于一个数字x,小明同学定义出了两个函数f(x)和g(x)。 f(x)表示把x这个数用十进制写出后各个数位上的数字之和。如f(123)=1+2+3=6。 g(x)表示把x这个数用二进制写出后各个数位上的数字之和。如123的二进制表示为1111011,那么,g(123)=1+1+1+1+0+1+1=6。 小明同学发现对于一些正整数x满足f(x)=g(x),他把这种数称为幸运数,现在他想知道,小于等于n的幸运数有多少个?
输入描述:
每组数据输入一个数n(n≤10)。
输出描述:
每组数据输出一行,小于等于n的幸运数个数。
样例输入:
21
样例输出:
3
解析
主要实现两个函数,分别求出一个数在十进制与二进制下各个位上数字之和,若和相同,说明该数为幸运数,则计数加一
采取递归的方式,对于第一个函数,求十进制的各个位上数字之和funf,先求当前数字的最后一位,再除去最后一位传参给函数本身求去除最后一位之后的最后一位;对于第二个函数fung,如果当前数字除2等于0则返回1递归结束,否则对当前数字模2,然后取模后除2传参给函数本身
————————————————
版权声明:本文为CSDN博主「Z-RICHARD」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43559540/article/details/100935545
代码
//
// Created by Theta on 2021/5/14.
//
#include <iostream>
using namespace std;
int fung(int i);
int funf(int i);
int main()
{
int num,count=0;
cin >> num;
for (int i = 0; i <= num; i++)
{
if (funf(i) == fung(i))
{
//cout << i << " ";
count++;
}
}
cout <<count;
return 0;
}
int funf(int n)
{//提取各个位数之和
if (n > 9)
{//若为两位数
return n % 10 + funf(n / 10);//从末位数开始加起
}
else
{
return n;
}
}
int fung(int n)
{
if (n / 2 != 0)
{
return n % 2 + fung(n / 2);
}
else
{
return 1;
}
}
3)求解回文序列问题
问题描述:
如果一个数字序列逆置之后跟原序列是一样的就称这样的数字序列为回文序列。例如:
{1, 2, 1}, {15, 78, 78, 15} , {112} 是回文序列,
{1, 2, 2}, {15, 78, 87, 51} ,{112, 2, 11} 不是回文序列。
现在给出一个数字序列,允许使用一种转换操作:
选择任意两个相邻的数,然后从序列移除这两个数,并用这两个数字的和插入到这两个数之前的位置(只插入一个和)。
现在对于所给序列要求出最少需要多少次操作可以将其变成回文序列。
输入描述:
输入为两行,第一行为序列长度n ( 1 ≤ n ≤ 50) 第二行为序列中的n个整数item[i] (1 ≤ iteam[i] ≤ 1000),以空格分隔。
输出描述:
输出一个数,表示最少需要的转换次数
示例
输入
4 1 1 1 3
输出
2
解析
这样的问题采用递归调用比较简单,而递归的核心思想:大问题转换为小问题加上特殊出口。
对输入的数组进行第一个数和最后一个数同时扫描,如果相等,跳过两个数的处理,第一位向后移动一位,最后一位向前移动一位。若不相等,第一种情况:第一位小于最后一位,则将第一位与它后一位相加为一个数,记录一次操作,进入递归。第二种情况:第一位大于最后一位:将最后一位与它前一位相加为一个数,记录一次操作,进入递归。
当前面扫描与后面扫描相遇时停止递归(特殊出口)。
————————————————
原文链接:https://www.cnblogs.com/fan979398/p/9826409.html
代码
#include<stdio.h>
int hui(int a[],int n,int i,int j,int t)
{
if(i>=j)return(t);
else if(a[i]==a[j])hui(a,n,i+1,j-1,t);
else if(a[i]<a[j])
{
a[i]+=a[i+1];
int k;
for(k=i+1;k<n;k++)
{
a[k]=a[k+1];
}
hui(a,n-1,i,j-1,t+1);
}
else if(a[i]>a[j])
{
a[j-1]+=a[j];
int k;
for(k=j;k<n;k++)
{
a[k]==a[k+1];
}
hui(a,n-1,i,j-1,t+1);
}
}
int main()
{
int n;
printf("序列个数:");
scanf("%d",&n);
int a[n];
int k;
printf("输入序列:");
for(k=0;k<n;k++)
{
scanf("%d",&a[k]);
}
printf("次数:%d",hui(a,n,0,n-1,0));
return 0;
}
4)棋盘覆盖
在一个\(2^k×2^k\) 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
解析
当k>0时,将\(2^k×2^k\)棋盘分割为4个\(2^{k-1}×2^{k-1}\) 子棋盘(a)所示。
特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,如 (b)所示,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘1×1。
具体过程
问题求解
下面介绍棋盘覆盖问题中数据结构的设计:
(1)棋盘:用二维数组Board[size][size]
表示一个棋盘,Board[0][0]
是棋盘的左上角方格。其中,size=\(2^k\)。为了在递归处理的过程中使用同一个棋盘,将数组Board设为全局变量;
(2)子棋盘:在棋盘数组Board[size][size]
中,由子棋盘左上角的下标tr
、tc
和棋盘边长s
表示;
(3)特殊方格:用Board[dr][dc]
表示,dr
和dc
是该特殊方格在棋盘数组Board中的下标;
(4)L型骨牌:一个\(2^k×2^k\)的棋盘中有一个特殊方格,所以用到L型骨牌的个数为\((4^k-1)/3\)将所有L型骨牌从1开始连续编号,用一个全局整型变量tile表示,其初始值为0。
代码
#include<iostream>
using namespace std;
int tile=1; //L型骨牌的编号(递增)
int b[100][100]; //棋盘
/*****************************************************
* 递归方式实现棋盘覆盖算法
* 输入参数:
* tr--当前棋盘左上角的行号
* tc--当前棋盘左上角的列号
* dr--当前特殊方格所在的行号
* dc--当前特殊方格所在的列号
* size:当前棋盘的:2^k
*****************************************************/
void chessBoard ( int tr, int tc, int dr, int dc, int size )
{
if ( size==1 ) //棋盘方格大小为1,说明递归到最里层
return;
int t=tile++; //每次递增1
int s=size/2; //棋盘中间的行、列号(相等的)
//检查特殊方块是否在左上角子棋盘中
if ( dr<tr+s && dc<tc+s ) //在
chessBoard ( tr, tc, dr, dc, s );
else //不在,将该子棋盘右下角的方块视为特殊方块
{
b[tr+s-1][tc+s-1]=t;
chessBoard ( tr, tc, tr+s-1, tc+s-1, s );
}
//检查特殊方块是否在右上角子棋盘中
if ( dr<tr+s && dc>=tc+s ) //在
chessBoard ( tr, tc+s, dr, dc, s );
else //不在,将该子棋盘左下角的方块视为特殊方块
{
b[tr+s-1][tc+s]=t;
chessBoard ( tr, tc+s, tr+s-1, tc+s, s );
}
//检查特殊方块是否在左下角子棋盘中
if ( dr>=tr+s && dc<tc+s ) //在
chessBoard ( tr+s, tc, dr, dc, s );
else //不在,将该子棋盘右上角的方块视为特殊方块
{
b[tr+s][tc+s-1]=t;
chessBoard ( tr+s, tc, tr+s, tc+s-1, s );
}
//检查特殊方块是否在右下角子棋盘中
if ( dr>=tr+s && dc>=tc+s ) //在
chessBoard ( tr+s, tc+s, dr, dc, s );
else //不在,将该子棋盘左上角的方块视为特殊方块
{
b[tr+s][tc+s]=t;
chessBoard ( tr+s, tc+s, tr+s, tc+s, s );
}
}
int main()
{
int t,size,x,y,total=0;
cin>>t; //输入的棋盘个数
while(t--)
{
cin>>size;
cin>>x>>y;
total++;
chessBoard (0,0,x,y,size );
cout<<"CASE:"<<total<<endl;
for ( int i=0; i<size; i++ )
{
for ( int j=0; j<size; j++ )
cout<<b[i][j]<<" ";
cout<<endl;
}
}
}
input sample
2
2
0 0
8
2 2
output sample
CASE:1
0 1
1 1
CASE:2
3 3 4 4 8 8 9 9
3 2 2 4 8 7 7 9
5 2 0 6 10 10 7 11
5 5 6 6 1 10 11 11
13 13 14 1 1 18 19 19
13 12 14 14 18 18 17 19
15 12 12 16 20 17 17 21
15 15 16 16 20 20 21 21
5)最接近点对问题
为了使问题易于理解和分析,先来考虑一维的情形。此时,S中的n个点退化为x轴上的n个实数 \(x_1,x_2,…,x_n\)。最接近点对即为这n个实数中相差最小的2个实数。
假设我们用x轴上某个点m将S划分为2个子集S1和S2 ,基于平衡子问题的思想,用S中各点坐标的中位数来作分割点。
递归地在S1和S2上找出其最接近点对{p1,p2}和{q1,q2},并设d=min{|p1-p2|,|q1-q2|}。
S中的最接近点对或者是{p1,p2},或者是{q1,q2},或者是某个{p3,q3},其中p3∈S1且q3∈S2。
能否在线性时间内找到p3,q3?
如果S的最接近点对是{p3,q3},即|p3-q3|<d,则p3和q3两者与m的距离不超过d。
即p3∈(m-d,m],q3∈(m,m+d]。
由于在S1中,每个长度为d的半闭区间至多包含一个点(否则必有两点距离小于d)
并且m是S1和S2的分割点,因此(m-d,m]中至多包含S中的一个点。由图可以看出,如果(m-d,m]中有S中的点,则此点就是S1中最大点。✔
因此,我们用线性时间就能找到区间(m-d,m]和(m,m+d]中所有点,即p3和q3。从而我们用线性时间就可以将S1的解和S2的解合并成为S的解。