回溯算法

1.什么是回溯

顾名思义,回溯就是回头的意思。比如我们要达到一个目的地,但是我们面前有三条路,并且只有一条路是可以到达目的地的,因此我们需要一条一条去尝试,如果不行的话就得回到原点,选择下一条路进行尝试,这种就叫做回溯。
在我们做题中,常见的组合问题,路径问题,使用回溯法,往往事半功倍。
在这里插入图片描述

2.解题方法

1.将问题描述成树形结构
描述成树形结构有利于寻找思路

2.通过树的关系,找出子问题的递归表达式
写出递归方程,寻找可行解

2.看是否满足剪枝条件
即通过分析问题树状结构,可以明显的找到不符合条件的情况,此时就直接终止当前递归,提高效率,这就是剪枝

3.实战演练

3.1路径的总和

题目:
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。

题目链接

在这里插入图片描述

bool GetSum(struct TreeNode* root, int targetSum,int count)
{
    if(root==NULL)//到达空节点说明上一节点不成立
        return false;

    count+=root->val;//将当前的值相加

    if(root->left==NULL&&root->right==NULL&&count==targetSum)//到达叶子节点
        return true;


    return GetSum(root->left,targetSum,count)||GetSum(root->right,targetSum,count);   
}

bool hasPathSum(struct TreeNode* root, int targetSum){
   
    //二叉树,首先确定一种遍历的方法,遍历一般都是前/中/后序递归
    //通过回溯剪枝的方法,如果当前路径上面的值,大于目标值则直接退出(有负数,不能用)
    //回溯需要一个变量保存当前节点的值,如果下一节点不满足条件,则进行剪枝
    //由于这里条件特殊不能进行剪枝处理

    if(root==NULL)
        return false;

    int  count=0;
    
    return GetSum(root,targetSum,count);

}

3.2路径总和貮

题目:
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径
题目链接

这里是上面那题的变种,因为上面是只需要判断是否有这么一条路径即可,而这个题目需要将所有路径保存起来,因此需要开辟一个二维数组来记录保存的值,还需要一个临时的数组和下标,因为没达到叶子节点前是不清楚当前路径需不需要进行保存的。我们可以将走过的路径都保存在临时数组中,进行一个判断,如果满足当前的条件,就将临时数组拷贝到二维数组中

void GetRoad(struct TreeNode* root, int targetSum, int* returnSize, int** returnColumnSizes,int **ret,int* temp,int count,int sub)
{
    if(root==NULL)//前面一个节点就已经不正确
        return ;

    temp[sub++]=root->val;//保存当前节点

    count+=root->val;

    if(root->left==NULL&&root->right==NULL&&count==targetSum)//当前路径合法
    {
        ret[*returnSize]=(int*)malloc(sizeof(int)*sub);//开辟空间

        memcpy(ret[*returnSize],temp,sub*sizeof(int));//拷贝

        returnColumnSizes[0][*returnSize]=sub;//保存当前二维数组中一维数组元素的个数
        (*returnSize)++;//该数组元素保存下来   
        return ;
    }


    GetRoad(root->left,targetSum,returnSize,returnColumnSizes,ret,temp,count,sub);//前序遍历
    GetRoad(root->right,targetSum,returnSize,returnColumnSizes,ret,temp,count,sub);

}


int** pathSum(struct TreeNode* root, int targetSum, int* returnSize, int** returnColumnSizes){

    *returnColumnSizes=(int *)calloc(5000,sizeof(int));//用来记录,路径中,节点个数
    *returnSize=0;//路径条数

    int **ret=(int **)malloc(sizeof(int)*1000);//放路径首元素地址

    int *temp=(int *)malloc(1000*sizeof(int));//临时记录路径数组


    int count=0;//统计数目
    int sub=0;//统计节点个数
    
    GetRoad(root,targetSum,returnSize,returnColumnSizes,ret,temp,count,sub);
    
    return ret;

}

3.3子集

题目:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

题目链接
在这里插入图片描述

void GetArray(int* nums, int numsSize, int* returnSize, int** returnColumnSizes, int **ret, int *temp,int temp_sub,int sub)
{
		ret[*returnSize] = (int *)malloc(sizeof(int*)*temp_sub);//给二级指针内的一级指针分配空间,第一次分配的是空

		memcpy(ret[*returnSize], temp, sizeof(int)*temp_sub);//将一维数组的内容填入到二维数组之中

		(*returnColumnSizes)[*returnSize] = temp_sub;//填入当前二维数组中一维数组的元素个数即当前子集的元素个数

		(*returnSize)++;//returnSize为二维数组的行下标,表示二维数组中有多少个元素

		for (int i = sub; i < numsSize; i++)
		{
			temp[temp_sub] = nums[i];//填入子集

			GetArray(nums, numsSize, returnSize, returnColumnSizes, ret, temp, temp_sub+1, i + 1);
            //temp_sub+1保证了,下一个元素是进行插入,i+1保证了填入临时数组内的元素不与上一个重复
		}
}


int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes)
{

	//**returnColumnSizes表示的是二维数组中每个一维数组元素中元素的个数

	*returnColumnSizes = (int *)calloc(2000,sizeof(int));//给二级指针中的一级指针分配空间,并且初始化为0
	
	int **ret = (int **)malloc(sizeof(int*)* 2000);//nums最多10个元素,即有......种解
	
	*returnSize = 0;//二维数组中,一维数组的个数

	int *temp = (int *)malloc(sizeof(int)*10);//每种解最多有10个元素
    
	GetArray(nums, numsSize, returnSize, returnColumnSizes, ret, temp,0, 0);

	return ret;
}

3.4全排列

题目:
给定一个 没有重复 数字的序列,返回其所有可能的全排列。

题目链接

在这里插入图片描述

void GetNum(int* nums,int *arr, int numsSize, int* returnSize, int** returnColumnSizes,int **ret,int *temp,int temp_sub)
{
    if(temp_sub==numsSize)//当元素满足后填充至返回数组
    {
        ret[*returnSize]=(int*)malloc(sizeof(int)*numsSize);//给二级指针内的一级指针开辟空间
        memcpy(ret[*returnSize],temp,sizeof(int)*numsSize);//内容填充
        (*returnColumnSizes)[*returnSize]=temp_sub;
        (*returnSize)++;//行坐标下移

        return;
    }

        for(int i=0;i<numsSize;i++)//每次都从0下标开始读取元素,同时有标记数组判断是否可以读取
        {
            if(!arr[i])
             {
                temp[temp_sub]=nums[i];
                arr[i]=1;//进行标记
             }
             else
             continue;

           GetNum(nums,arr,numsSize, returnSize, returnColumnSizes,ret,temp,temp_sub+1);

           arr[i]=0;//取消标记
        }    

}
int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){

    int **ret=(int **)malloc(sizeof(int*)*1000);
    *returnSize=0;//行坐标
    *returnColumnSizes=(int *)calloc(1000,sizeof(int));//列坐标

    int *temp=(int *)malloc(sizeof(int)*1000);//临时装纳数组元素

    int *arr=(int *)calloc(numsSize,sizeof(int));//标记数组,用0,1来进行标记

    GetNum(nums,arr,numsSize, returnSize, returnColumnSizes,ret,temp,0);
    
    return ret;

}

3.5全排列貮

题目:
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

题目链接

没错,这题是上题的变种,只是由无重复元素的数组改成了含有重复元素的数组。
具体思路还是和上题的是一样的,只是多增加了一步,我们需要排重,即进行剪枝处理。

如何剪枝:如果数组的元素是杂乱无章的,我们需要将所有的结果拿到后,再进行遍历排重,这样就比较麻烦了。我先给出结论,我们可以将数组排序,让相同的元素在一起,如果填入临时数组的元素和上一个没有标记的元素相同,那么我们就直接剪枝,接下来我证明一下这是为什么。

在这里插入图片描述

void GetNum(int *nums,int *arr, int *temp,int numsSize, int *returnSize,int **returnColumnSizes,int **ret,int temp_sub)
{
    if(temp_sub==numsSize)
    {
        ret[*returnSize]=(int *)malloc(sizeof(int)*numsSize);
        memcpy(ret[*returnSize],temp,sizeof(int)*numsSize);
        returnColumnSizes[0][*returnSize]=numsSize;
        (*returnSize)++;

        return ;
    }

    for(int i=0;i<numsSize;i++)
    {
    if(i-1>=0&&arr[i-1]==0&&nums[i-1]==nums[i])//重复元素,剪枝
        continue;

        if(!arr[i])
        {
            temp[temp_sub]=nums[i];
            arr[i]=1;
        }
        else
        continue;

     GetNum(nums,arr, temp,numsSize, returnSize,returnColumnSizes,ret,temp_sub+1);
     arr[i]=0;

    }
}

int  ComInt(const int *x,const int *y)//,传进来的是一个序列之中,任意一个元素的地址
{
	return *x-*y;
}

int** permuteUnique(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
   
    qsort(nums,numsSize,sizeof(int),ComInt);//排序


    int **ret=(int **)malloc(sizeof(int)*2000);
    *returnSize=0;
    *returnColumnSizes=(int *)calloc(2000,sizeof(int));
    int *temp=(int*)malloc(sizeof(int)*2000);

    int *arr=(int *)calloc(2000,sizeof(int));//标记数组

    GetNum(nums,arr, temp,numsSize, returnSize,returnColumnSizes,ret,0);

    return ret;

}

3.6组合

题目:
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

题目链接

找到递归出口:即临时数组中元素个数达到k个,将内容拷贝给放回数组,退出当前递归
由于是组合问题,我们需要注意重复问题,因此通过改变循环的起点,来控制元素重复的子集的产生

void GetNum(int n, int k, int * returnSize,  int **returnColumnSizes,int *temp,int **ret,int temp_sub,int sub)
{
    if(temp_sub==k)//将满足条件的数组拷贝至返回数组
    {
        ret[*returnSize]=(int *)malloc(sizeof(int)*k);
        memcpy(ret[*returnSize],temp,sizeof(int)*k);
        returnColumnSizes[0][*returnSize]=k;
        (*returnSize)++;
        return;
    }

    for(int i=sub;i<=n;i++)
    {
        temp[temp_sub]=i;

        GetNum(n,  k, returnSize, returnColumnSizes,temp,ret, temp_sub+1,i+1);//i+1保证不会往回走
    }

}

int** combine(int n, int k, int* returnSize, int** returnColumnSizes){

    //准备二维数组
    int **ret=(int **)malloc(sizeof(int)*15000);
    *returnSize=0;
    *returnColumnSizes=(int *)malloc(sizeof(int)*15000);

    int *temp=(int *)malloc(sizeof(int)*k);//临时保存K个数的数组

    GetNum( n, k,  returnSize,  returnColumnSizes,temp,ret,0,1);

    return ret;

}
  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值