2.递归(分治)1️⃣(含二维递归)

 🤌算法初步-递归 

框架:if  {return} (base case最简单/基础的情况,可能不止一个);   else {return} (recursive case); 递归思想合理性的数学支撑-数学归纳法

递归是解题思路--分而治之思想的代码实现之一-->看算法笔记递归部分

递归函数:不需要考虑递归函数是如何实施的,只需要考虑它该做什么!!一般而言题目要求什么就定义它的功能是什么

base case (递归边界):思考角度1.递归到哪种情况下结束,如果一下想不清楚可以从recursive case 往后倒推<-->2.最简单的情况

递归表达式:f(n) = xxxxxxf(n-1)xxxx,也就是需要思考如何构建含有f(n-1)的表达式才能得到f(n);

 1.最简单的递归:简单模拟,题目已经给出了递归的边界(base case)和递归表达式(recursive case),按照它写就完了,比如斐波那契。
2.需要稍微思考递归边界(base case)和递归表达式:

  递归函数:需要思考用什么(下标/个数/大小。。。)作为它的参数;一般而言题目要求输出的数据是什么类型,递归函数就定义为改类型,也即直接返回要求输出的数据(除了二维递归,它一般用全局数组记录结果,所以二维递归的递归函数一般定义为void)

  递归表达式:

      子问题(与原问题比:规模更小且具有相同结构): f(n-1)

        f(n) = xxxxxxf(n-1)xxxx,也就是需要思考如何构建含有f(n-1)的表达式才能得到f(n);

  递归边界:无法再分割的情况

    (1)用递归实现 返回一个反转字符串:

注意,递归函数reverseStr(int n)的参数n代表的是:字符个数,n个字符

(2)求完全二叉树路径的最大值

晴问算法数塔,设树深度为n

难点:

  数据存储:完全二叉树可用正方形数组a[n+1][n+1] (i/j从下标1开始存储)存,用a[i][j](数组的第i行第j列)存储树的第i行第j列数据;第i行只有i个数据;

  递归表达式:假设塔顶是a[i][j], 则它的左子树塔顶为a[i+1][j], 右子树塔顶为a[i+1][j+1],  那么从该塔顶到塔底路径的最大值为F( i, j )= a[i][j]. + max(  F(i+1, j),  F(i+1, j+1) ), 也因此递归函数的参数可用int i, int j;

  递归边界:当塔顶是完全二叉树树底其中之一的元素,即i == n时,F[i][j] = a[i][j];

(3)可以考虑两种递归边界的情况

🌰用递归判断回文字符

  因为要求判断,所以递归函数的类型可为bool F(.    );

  难点:

                递归边界:两个字符的情况即j == i+1,   F(i, j) =. s[i] == s[j];

                                   一个字符的情况即 i== j ,F(i, j) == true;

                              然后集成:i >= j时,返回true;

                  递归表达式:递归边界以外的情况,F(i, j) =    (s[i] == s[j])  and  F(i+1, j-1) ;  

  🌰上楼梯,最后一次抬脚可上一级/两级台阶,(类汉诺塔问题,解题思路一致,详见cs61a)

递归边界可以集成为if(n <= 1),因为当n==2时,count(2) = count(1) + count(0)

(4) 汉诺塔问题(详见cs61a 递归部分视频)

A柱起始有n个盘,先把上面n-1个盘放到B(辅助柱temp),再把最后一个放到C, 最后把temp柱子的n-1个移到C。(三步)

  @要点:2个盘子及以上才需要借助第三方柱子()

  @难点:将以上具体的柱子用代数抽象成三个部分,也即char a, char b, char c , 三个抽象的代数。

  ~递归表达式:       H(int n, char a, char b, chat c),

                                H( n-1, a, c, b )把from上的n-1(当作一个整体)个小盘子,借助c移动到b上;

                                H.move(1, a  c);把a最底下的一个盘子,移动到c,题目要求我们打印出每一步骤:则该句转化的代码是:printf("  ->. ",a, c);

                                H(n-1, b ,a, c)

  ~递归边界:        n == 1时,不需要借助第三方柱子,直接移动即可。(直接打印printf("  ->. ",from, to);) 

c语法:printf("%d\n",(int)pow(2.0, n*1.0)-1 ); #include <cmath>

3.👀需要给递归表达式套上循环的复合处理
 (1)🌰自然数分解之最大积

主思想:对自然数 n 的分解,有两“种”分解方案:i+(n-i)  和. i+F(n-i) 。递归的每一步都是把 n 减去某个数 i,然后继续递归分解 n-i。这就相当于在每一步确定了分解中的一个数,然后将剩余的部分继续递归分解。
递归函数:题目要求乘积最大值,那就定义递归函数为int f(int n) 并返回对n分解后的乘积最大值;
递归边界:明显当n == 1时,无法再对1分割,即边界为:if(n == 1) return 1; 
 递归表达式:如何从f(n-1)构建出表达式等于f(n),有“两种”分解乘积方案: i * (n-i) 和 i* f(n-1), 所以f(n) = max ( i* (n-i),  i*f(n-i)  ) = i * max( n-i, f(n-i) ).
#include <iostream>
#include <algorithm>
using namespace std;
//递归函数直接返回要输出的数据
int f(int n) {

    if(n == 1)
        return 1;
    else
        {
            int Current_max = 1;
            for(int i = 1; i < n; i++)
                Current_max = max(Current_max, i * max(n-i, f(n-i)));
            return Current_max;
        }
}

int main() {
    int n;
    scanf("%d", &n);
    printf("%d\n", f(n));
    return 0;
}
 (2)🌰自然数分解之方案数

难点:如何解决重复出现的问题-->限制最大拆分数增加一个参数bound,F(n, bound)

当前自然数 n 的分解问题,递归地减少为分解更小的自然数 n-i 的问题。例如,假设 n=5,选择 i=3,则问题变成了分解 2,递归调用 F(2, 3)。而• bound参数是为了避免出现重复分解。通过限制当前可用的最大值不超过 i,确保每次递归分解方案是非递增顺序的。

bound 参数的作用是在递归调用时,限制下一步递归中允许选择的最大值。

 代码具体执行流程图

但又出现了第一“种”分解方案重复的问题,因此也要对它做限制让它递减(i >= n-i ) cnt才能++。注意点:第一种分解方案还要判断n-i > 0,保证n-i是正数

4.二维递归
 (1)🌰棋盘覆盖问题

2^k x 2^k 的棋盘

用数学归纳法证明该问题一定有解:

        k = 1时,该问题有解; 假设k = n时,该问题有解;证明k = n+1有解:

2^(n+1)x 2^(n+1)可以划分成4个2^n x 2^n的小棋盘,特殊方格一定在其中一个小棋盘里,则该小棋盘有解,现在提前将一块骨牌放在另三个小棋盘的交汇处,如图:

则这三个小棋盘都有特殊方格了,也就都转化为了规模为n的子问题(有解)

要求:

❗️❗️代码层面的分治思维:
   子问题:和主问题具有相同结构的规模更小的问题, 因此递归函数要有表示规模大小的参数!
   一维切一刀(分成两个子问题),二维切两刀(分成4个子问题-->一个子问题用一个递归函数)
   所以二维的分治要分别处理这四个子问题,对于该题四个子问题即四个小棋盘,❗️❗️把一个小棋盘(一个子问题)递归解决了,再去处理另一个小棋盘(另一个子问题)-----> 对于子问题1左上小棋盘,有两种可能:one特殊方格在左上小棋盘,那么能到得一个拐角坐标,并将左上小棋盘作为母问题递归;two特殊方格不在左上小棋盘,那么将该小棋盘的右上角坐标作为“特殊方格”把它作为母问题递归; ----->由此部分倒推l == 1的时候递归结束,因为它的上一个递归函数l == 2时正好骨牌全部覆盖完。

因此:为什么以上验证能想到要提前放一个骨牌去覆盖,因为我们需要将另三个小棋盘转化为规模为n的子问题,也就是要让这三个小棋盘也有“特殊方格”,所以我们可以想到提前放一个骨牌去覆盖交汇处,把骨牌拆成分别在三个小棋盘里的“特殊方格" -----> 一举双雕:1.得到了一个拐角坐标;2.把问题规模化小了

⚠️❗️规模参数与原点:因为该题目与二维坐标有关(也因此可用结构体),为了表示每个格子的坐标(原点坐标+偏移),需要设定一个原点(x,y),本题设定原点为最左下的格子,偏移用长度表示, 因此规模参数设为棋盘边长l , 所以每个子问题(共四个小棋盘)的规模为kid_l = l /2 。设定表示原点的坐标一般来说不可少(而且应该将表示原点坐标的x,y与规模参数一起作为递归函数的参数),因为子问题分布在同一平面的不同地方,需要初始原点+偏移才能指示到位于不同地方的子问题(初始原点+偏移就是该子问题的原点了);---->递归子问题时,要更新规模参数和原点到与子问题相匹配。

递归边界:若递归函数设为void,则递归边界是l == 1, retuen ;

递归表达式:1...............2.     3...      4......:

代码:

//cards[MAXN]:因为题目要求输出每个骨牌的坐标,所以用它记录每个骨牌的拐角坐标

//MAXN: 每种规模都需要(2^k x 2^k -1)/3个骨牌才能覆盖,k<=8

//l:当前处理棋盘的边长

//(cx, cy):特殊格子的坐标

//(x, y):当前处理棋盘的原点格子

对于子问题1左上小棋盘,有两种可能:one特殊方格在左上小棋盘,那么能到得一个拐角坐标,并将左上小棋盘作为母问题递归;two特殊方格不在左上小棋盘,那么将该小棋盘的右上角坐标作为“特殊方格”把它作为母问题递归; ----->由此部分倒推l == 1的时候递归结束,因为它的上一个递归函数l == 2时正好骨牌全部覆盖完。

#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = (256 * 256 -1 ) / 3;
struct Point{
    int x;
    int y;
    Point(){};
    Point(int _x, int _y) {x = _x; y = _y;};
} corner[MAXN];
bool cmp(Point a, Point b)
{
    if(a.x != b.x)
        return a.x < b.x;
    else
        return a.y < b.y;
}
int num = 0;
void cover(int x, int y, int l, int cx, int cy)
{
    if(l == 1)//因为上一个递归函数l==2, 即骨牌都覆盖完了
        return ;

    int half = l / 2;
    //for kid problem1-left down,has two situations:
    if(cx < x+half && cy < y+half)//one
    {
        corner[num++] = Point(x+half, y+half);
        cover(x, y, half, cx, cy);
    }
    else //two
        cover(x, y, half, x+half-1, y+half-1);
    //              left top,
    if(cx < x+half && cy >= y+half)
    {
        corner[num++] = Point(x+half, y+half-1);
        cover(x, y+half, half, cx, cy);
    }
    else
        cover(x, y+half, half, x+half-1, y+half);
    //              right top,
    if(cx >= x+half && cy >= y+half)
    {
        corner[num++] = Point(x+half-1, y+half-1);
        cover(x+half, y+half, half, cx, cy);
    }
    else
        cover(x+half, y+half, half, x+half, y+half);
    //              right down,
    if(cx >= x+half && cy < y+half)
    {
        corner[num++] = Point(x+half-1, y+half);
        cover(x+half, y, half, cx, cy);
    }
    else
        cover(x+half, y, half, x+half, y+half-1);
}
int main(){
    int k, cx, cy;
    cin >> k >> cx >> cy;
    cover(1,1, (int)pow(2.0, k), cx, cy);
    sort(corner, corner+num, cmp);
    for(int i = 0; i < num; i++)
        cout << corner[i].x << " " << corner[i].y << endl;
}
语法知识点复习:

1.<camth>:

2.sort( , , cmp)头文件:

3.结构体初始化:

(2)🌰数字螺旋矩阵
主思想:参考棋盘覆盖的规模参数原点的 规模参数部分;因为题目要求输出矩阵,所以第一步先定义一个矩阵存数字。

难点:如何将数字存入二维数组(分四种情况);递归边界要考虑两种情况-->但代码可以集成为<= 0,因为n == 1时,recursive case依旧可以处理,处理完后n更新为-1

注意点:第一个循环(称上的循环)存入了一整行(第一行)的数字,所以右的循环应该从数组第二行的最右格开始往下存数字,同理下循环,左循环应该从倒二行往上到第二行,以此类推。。。

#include <iostream>
using namespace std;
const int MAXN = 25;
int a[MAXN][MAXN];
int idx = 1;
void spiral(int l, int x, int y)
{
    //递归边界(递归结束时有两种情况,当n是奇数/偶数
    if(l == 0) //or :      if( l <= 0) { return ;}
        return ;
    else if(l == 1)
        a[x][y] = idx;
    else
    {
        for(int j = y; j < y+l; j++)
            a[x][j] = idx++;
        for(int i = x+1; i < x+l; i++)
            a[i][y+l-1] = idx++;
        for(int j = y+l-2; j >= y; j--)
            a[x+l-1][j] = idx++;
        for(int i = x+l-2; i > x; i--)
            a[i][y] = idx++;
        int kidl = l-2;
        spiral(kidl, x+1, y+1);
    }
}
⚠️输出注意:要求输出数字螺旋矩阵。行末不允许有多余的空格。要考虑特殊情况:只有一个元素即n == 1时;记住用以下代码:(j < n-1即j不是最后一个元素则输出空格,否则输出\n)
for(int i = 0; i < n; i++) {
        for(int j = 0; j < n; j++) {
            printf("%d", a[i][j]);
            printf(j < n - 1 ? " " : "\n");
        }
 🎁不用递归函数的解法:

定义上下左右边界,循环存入并更新上下左右边界,不断逼近直到有边界交叉则说明遍历结束

用四个变量分别代表四个边界,实时更新:

#include <iostream>
using namespace std;
const int MAXN = 25;
int a[MAXN][MAXN];
int idx = 1;
void spiral_2(int n)
{
   int up = 0;//上边界
   int down = n-1;//下边界
   int left = 0;
   int right = n-1;
   if(n == 1) 
   {
       a[0][0] = idx++;
       return ;
   }
   if(n == 0) return ;
   while(true)
   {
       for(int j = left; j <= right; j++)    a[up][j] = idx++;
       if(++up > down) break;//更新上边界,若上边界在下边界下面则遍历结束

       for(int i = up; i <= down; i++)      a[i][right] = idx++;
        if(--right < left) break;

       for(int j = right; j >= left; j--)   a[down][j] = idx++;
       if(--down < up) break;

       for(int i= down; i >= up; i--)       a[i][left] = idx++;
       if(++left > right) break;
   }
   return ;
}

int main()
{
    int n;
    scanf("%d", &n);
    spiral_2(n);
    for(int i = 0 ; i < n; i++)
        for(int j = 0; j < n; j++)
        {
            printf("%d", a[i][j]);
            printf(j < n-1 ? " ":"\n");//
        }
}
(3)🌰盒分形

主思想:❗️❗️代码层面的分治思想(但这题更简单,给你划分好了5个子问题,直接对5个子问题上递归函数)+规模参数与原点 (规模参数设定为盒分形的边长l,n层:边长为3^(n-1);原点设置为盒分形的最左上格子,则初始原点即a[0][0])
代码:

说明:该题递归函数的规模参数也可以设定为F(n)的n,如:

void f(int n, int x, int y) {
    if (n == 1) {
        mp[x][y] = 'X';
    } else {
        int unit = (int)pow(3.0, n - 2);
        f(n - 1, x, y);                         // 左上
        f(n - 1, x, y + 2 * unit);              // 右上
        f(n - 1, x + unit, y + unit);           // 中间
        f(n - 1, x + 2 * unit, y);              // 左下
        f(n - 1, x + 2 * unit, y + 2 * unit);   // 右下
    }
//一层:边长为1; 二层:3; 三层:3*3 ;四层:9*3=3^3;
//n层:边长为3^(n-1)
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
const int MAXN = (int)pow(3, 6);
char a[MAXN][MAXN];
//规模参数l设定为该盒分形的边长
//原点设为最左上的格子
void F(int l, int x, int y)
{
    if(l == 1)
        {
            a[x][y] = 'X';//?
            return ;
         }
    else
    {
       int kid_l = l / 3;
       F(kid_l, x, y);//左上角的F(n-1)
       F(kid_l, x+kid_l,y+kid_l);//中间
       F(kid_l, x+2*kid_l,y);//左下角的
       F(kid_l, x, y+2*kid_l );//右上角的
       F(kid_l, x+2*kid_l, y+2*kid_l);//右下角的
    }
    return ;   
}
int main(){
    int n;
    scanf("%d", &n);
    memset(a, ' ', sizeof(a));
    int init_l = (int)pow(3.0, n-1);
    F(init_l, 0, 0);
    for(int i = 0; i < init_l; i++)
        {
            for(int j= 0; j < init_l; j++)
                printf("%c", a[i][j]);
         printf("\n");
        }
}
 知识点复习:

1.二维数组的初始化:头文件#include <cstring>

多维数组:当你初始化一个多维数组时,C++会期望你为每一维提供一组初始化值。

如果你写成char s[3][3] = {'a'};,这只会初始化第一个元素s[0][0]为'a',其余元素会初始化为0(即null字符\0)。

一维数组初始化:char a[MAXN] = {' '};

(4)🌰谢宾斯基地毯
主思想与难点:在盒分形的基础上,还需要在对子问题分别递归前,额外处理中间的X(n-1)图形:
int kid_l = l/3;
for(int i = x+kid_l; i < x+2*kid_l; i++)
   for(int j = y+kid_l; j < y+2*kid_l; j++)
         a[j][j] = 'X';
注意点:递归边界if(l == 1) { a[x][y] = ' '; return ;},是a[x][y] = ' ';(也即原点为' '),而不是[1][1]!   加号包裹:可以对数组初始化为全'+'号,原点定为左上角,则初始原点设为(1,1)。
代码:
//n层嵌套的图形边长l = 3^(n-1);
#include <iostream>
#include <cstring>
#include <cmath>
const int MAXN = (int)pow(3, 6) + 2;//加上➕,边长多2
char a[MAXN][MAXN];
void carpet(int l, int x, int y)
{
    if(l == 1)
        {
            a[x][y] = ' ';
            return ;
        }
    else
        {
            int kid_l = l/3;
            for(int i = x+kid_l; i < x+2*kid_l; i++)
                for(int j = y+kid_l; j < y+2*kid_l; j++)
                    a[i][j] = 'X';
            carpet(kid_l, x, y);
            carpet(kid_l, x, y+kid_l);
            carpet(kid_l, x, y+2*kid_l);

            carpet(kid_l, x+kid_l, y);
            carpet(kid_l, x+kid_l, y+2*kid_l);

            carpet(kid_l, x+2*kid_l, y);
            carpet(kid_l, x+2*kid_l, y+kid_l);
            carpet(kid_l, x+2*kid_l, y+2*kid_l);
        }
        return ;
}

int main(){
    int n;
    scanf("%d", &n);
    memset(a, '+', sizeof(a));
    int L = (int)pow(3.0, n-1);
    carpet(L, 1, 1);
    for(int i = 0; i < L+2; i++)
     {
         for(int j = 0; j < L+2; j++)
           printf("%c", a[i][j]);
        printf("\n");
     }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值