greenhand算法学习笔记(2023.9.25):递归

  • 递归思路:

  1. 明确问题目标:想清楚自己要解决一个什么样的问题,该问题是否可以拆分成相似的子问题
  2. 找到回归条件:所谓回归条件,就是到达某一条件之后函数不能再递归下去,向上逐层返回结果
  3. 拆分问题:找到原问题等价的表达式,即将原问题拆分成和原问题相似的子问题
  • 例题分析

1.整数拆分问题

问题:一个整数n,拆分成若干个小于等于m的整数相加的形式,并且左侧的数字要大于右侧的数字,数字顺序颠倒算同一种拆分形式。

(做一下这篇文章的学习笔记,自己做题没看懂,看了这个博主的回答豁然开朗)

整数拆分问题的四种解法_尼奥普兰的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/u011889952/article/details/44813593例:

6=6

    5+1

    4+2,4+1+1

    3+3,3+2+1,3+1+1+1

    2+2+2,2+2+1+1,2+1+1+1+1

    1+1+1+1+1+1

一共有11种拆分方法。

思路:

首一,一个可以拆分的整数,其拆分得到的整数也可以拆分(n≥3),所以这可以用递归解决。

第二,当n=1时,只有n=n一种情况,不能再拆分,即;当m=1时,不管n多大,都只有一种情况,即n个1相加,后续不能继续拆分;这两种情况就是该问题的回归条件。

第三,将问题拆分。

  1. 当n=m时,如果拆分种含有n,就只有一种情况;如果不含n,则所拆分得到的数一定不大于n-1,就变成了把n拆分成若干个不大于n-1的整数相加的问题,即f\left ( n,n-1 \right ),一共有f\left ( n,n-1 \right )+1种情况。
  2. 当n>m>1时,①如果拆分中包含m,得到的一组拆分中仍有可能存在m,所以变成了整数n-m拆分成不大于m的若干个数相加的问题,即f\left ( n-m ,m\right )种情况;②如果不包含m,则拆分得到的一组整数都不大于m-1,所以问题变成了将n拆分成若干个不大于m-1的整数的问题,即f\left ( n,m-1 \right ),一共有f\left ( n-m,m \right )+f\left ( n,m-1 \right )种情况。
  3. 当n<m时,仍然是f\left ( n,n \right )的问题。
代码
int GetPartitionCount(int n,int m) {
	if (n==1 || m==1) {//if n==1 or m==1
		return 1;
	}
	else if (n == m) {
		return GetPartitionCount(n, n - 1) + 1;
	}
	else if (n > m&&m > 1) {
		return GetPartitionCount(n, m - 1) + GetPartitionCount(n - m, m);
	}
	else if (n < m) {
		return GetPartitionCount(n, n);
	}
}

2.棋盘覆盖问题

算法分析与设计:棋盘覆盖问题(分治法)_棋盘覆盖问题算法分析_SongXJ--的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/SongXJ_01/article/details/112439322

思路
  1. 将问题转化为递归问题:要将问题拆分成几个相同的子问题,所以对于其他三个没有空缺的子棋盘,利用L形骨牌填充到三个子棋盘交会的顶点,如此一来四个子方块都有一个空缺的子棋盘。
  2. 分情况依次递归:按照从左向右,从上到下的顺序依次递归四个子棋盘,每次将棋盘四等分,有空缺的子棋盘继续递归,填充其他三个没有空缺的棋盘,直到子棋盘的大小为1,每次递归后骨牌数加一(后++)。
源码:
#include <iostream>
#include <iomanip>
using namespace std;

int tile = 1;        // 骨牌序号
int** board; // 二维数组模拟棋盘

// (tr,tc)表示初始棋盘左上角的位置, (dr,dc)表示空格子的位置, size=确定棋盘大小,为2的整数次幂
void chessBoard(int tr, int tc, int dr, int dc, int size){
	if (size == 1)//回归条件
		return;
	int s = size / 2; //四分
	int t = tile++;   //t记录序号
	// 判断特殊方格在不在左上角棋盘
	if (dr < tr + s && dc < tc + s){//在,就继续递归
		chessBoard(tr, tc, dr, dc, s);
	}
	else{//不在,则填充其他子棋盘
		board[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{
		board[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{
		board[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{
		board[tr + s][tc + s] = t;//覆盖左上角
		chessBoard(tr + s, tc + s, tr + s, tc + s, s);
	}
}

int main(){
	int boardSize = 0; 
	cout << "请输入棋盘边长(2的整数次幂):";
	cin >> boardSize;
	board = new int*[boardSize];
	for (int i = 0; i < boardSize; i++) {
		board[i] = new int[boardSize];
		for (int j = 0; j < boardSize; j++) {
			board[i][j] = 0;
		}
	}
	int dr=0, dc = 0;
	cout << "请输入空格子在棋盘中的行、列位置:" << endl;
	cin >> dr >> dc;
	chessBoard(0, 0, dr-1, dc-1, boardSize); // (0, 0)为顶点
	//输出编号
	int i, j;
	for (i = 0; i < boardSize; i++){
		for (j = 0; j < boardSize; j++){
			cout <<setw(5)<< board[i][j] ;
		}
		cout << endl;
		cout << endl;
	}
	return 0;
}

3.循环赛日程表问题

参考:

算法设计与分析——循环赛日程表_循环日程赛算法_何智鹏的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_44469806/article/details/108936037?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169494939716800185857243%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169494939716800185857243&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-2-108936037-null-null.142%5Ev94%5EchatsearchT3_1&utm_term=%E5%BE%AA%E7%8E%AF%E8%B5%9B%E6%97%A5%E7%A8%8B%E8%A1%A8&spm=1018.2226.3001.4187

代码思路:
  1. 最外层循环(m)控制拆分得到的子棋盘的大小:①控制行列数的初始值;②控制不同大小的子棋盘需要进行几次对角填充。
  2. 第二层循环(t)控制对角填充时需要多少内层循环几次才能将1个划分的子棋盘填充完整。
  3. 最内层的2层循环(i,j)控制子棋盘中逐方块填充。
#include <iostream>
#include <iomanip>
using namespace std;

int** table;

//非递归
void roundRobin(int n) {
	for (int i = 0; i < n; i++) {//初始化数组第0行
		table[0][i] = i + 1;
	}
	for (int m = 1; m < n; m=m<<1) {//m代表该划分的棋盘边长,每循环依次扩大1倍
		for (int t = 1; t <= n / (2*m); t++) {//t代表m级别的划分有几个->一共要做几次交换
			for (int i = m; i < 2 * m; i++) {//row控制需要填充的行位置
				for (int j = m; j < m * 2; j++) {//col控制需要填充的列位置
					//左下角与右下角、左上角与右上角行数相同
					//右下角=左上角
					table[i][j + (t - 1)* m * 2] = table[i - m][j + (t - 1)*m * 2 - m];
					//左下角=右上角
					table[i][j + (t - 1)* m * 2 - m] = table[i - m][j + (t - 1) * 2 * m];
				}
			}
			
		}
	}
}

int main()
{
	int k = 0, num = 0;
	cout << "输入选手个数:";
	cin >> num;//选手个数
	//为二维数组分配内存
	table = new int*[num];
	for (int i = 0; i < num; i++) {
		table[i] = new int[num];
	}
	roundRobin(num);
	for (int i = 0; i < num; i++) {
		for (int j = 0; j < num; j++) {
			if (j == 0) {
				cout << setw(4) << table[i][j] << " |";
			}
			else {
				cout << setw(4) << table[i][j];
			}
		}
		cout << endl;
	}
	return 0;
}

对以上i和j的初始值的问题和对角替换部分的代码进行解释:

(想不到很好的解释办法,先用图表示一下)

4.发帖水王

问题描述:

有某一论坛,其中有一发帖水王,不但喜欢发帖,还会回复其他ID发的每个帖子。“水王”发帖数目超过了帖子总数的一半。如果你有一个当前论坛上所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,你能快速找出这个传说中的Tango水王吗?

拓展:

如果“水王”没有了。但是有3个发帖很多的ID,发帖数目都超过了帖子总数的1/4。如何快速找出他们的ID?

思路:

思路可以用一句话总结:(每个)发帖水王的发帖数-非水王的发帖数总数>0

如果只有1个水王,他的发帖数一定大于1/2,则非水王的发帖总数一定小于1/2;同理,如果有3个水王,每个水王的发帖总数大于1/4,则非水王的发帖总数一定小于1/4,那么每个水王的发帖数-非水王的发帖总数>0。

源码:
#include <iostream>
using namespace std;

int candidates[3] = { 0,0,0 };
int times[3] = { 0,0,0 };

void find(int ID[], int num) {	
	for (int i = 0; i < num; i++) {
		bool change = false, same = false;
		for (int j = 0; j < 3; j++) {
			if (candidates[j] == ID[i]) {//与某个候选者相同
				times[j]++;
				same = true;	
				break;
			}
		}
		if (!same) {//与三个候选者都不同
			for (int j = 0; j < 3; j++) {
				if (times[j] == 0) {//判断是否有times==0的情况
					candidates[j] = ID[i];
					times[j] = 1;
					change = true;//发生替换
					break;
				}
			}
			if (!change) {//没有times==0的候选者
				for (int j = 0; j < 3; j++) {
					times[j]--;
				}
			}
		}
		
		
	}
}

int main()
{
	//int num = 20;
	//int ID[20] = { 2,3,3,1,2,1,4,3,3,1,1,1,2,5,3,3,1,2,2,2 };
	int num = 16;
	int ID[16] = { 1, 2, 3, 4, 2, 3, 1, 1, 2, 3, 1, 2, 3, 1, 2, 3 };
	find(ID, num);
	cout << "发帖水王的ID为:" ;
	for (int i = 0; i < 3; i++) {
		cout << candidates[i] << "  ";
	}
	return 0;
}

(未完待续2023.9.25)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值