算法(5)-暴力搜索-排列、组合(子序列、子数组、子串、回文)

Abstract

常见 数组/字符串 找子【序列】、【子串、子数组】类题 是组合类题目,前者元素之间无需连续,后需要连续。在组成res的元组里,元素的相对位置不变,即res中只会出现元组(a, b, c),元组(b,a,c)不会出现在res中。

  1. 子序列–元素不需连续 – 最长递增子序列LIS、 最长公共子序列LCS
  2. 子串、子数组数 – 元素需要连续-- - 最大和子数组

排列是和组合共同进退的好朋友,只提其一,必随另一。 排列、组合问题常用解题思路暴力搜索回溯,如果外加求极值,大概率要增加dp

Introduction

排列:英文为 Permutation 或者 Arrangement,因此在数学公式里常用P 或者 A 表示。
排列数:从n个不同的元素中取m个元素,按一定的顺序组成的组合,成为一个排列;不同排列的总数为:
A n m = n ∗ ( n − 1 ) ∗ . . . . ∗ ( n − ( m − 1 ) ) = n ! ( n − m ) ! A_n^m = n * (n-1) * ....*(n-(m-1)) = \frac{n!}{(n-m)!} Anm=n(n1)....(n(m1))=(nm)!n!

  1. 第一个位置有n中选择,第二个位置只剩 n-1个选择,…
  2. {1, 2} 和 {2, 1} 可以构成不同的排列

组合:英文是 Combination,因此在数学公式里常用 C 表示。
组合数:从n个不同的元素里取m个元素,无序的放在一起,成为一个组合;不同的组合数为:
C n m = A n m m ! C_n^m = \frac{A_n^m}{m!} Cnm=m!Anm

  1. 第一个位置有n中选择,第二个位置只剩 n-1个选择,…
  2. {1, 2} 和 {2, 1} 是同一个组合。 m个数的排列方式有m!种,但是这m!种排列方式仅属于一个组合。所以 A n m A_n^m Anm 中 每一个批(m个)数字 都被多排列的m!次,相当于整体扩大了m!倍,所以组合数需要➗m!

组合数的性质:
在这里插入图片描述

算总数: 如何把所有的可能性 【不多】 【不少】的统计出来。

参考博文:2小时学会中学排列组合

1. 组合

通过保证元素之间的相对顺序不变来 防止出现重复的组合。即dfs start参数决定了for循环起点(多叉树扩展分支)

78. 子集-(无重复-不复选-不限长)

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

  1. 各个不同长度的组合 均为一个子集
class Solution {
    vector<vector<int>> _res;
    vector<int> _nums;
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        _nums.assign(nums.begin(), nums.end());
        vector<int> path;
        dfs(path, 0);
        return _res;
    }

    void dfs(vector<int>& path, int start) {
        _res.push_back(path);
        for (int i = start; i < _nums.size(); i++) {
            path.push_back(_nums[i]);
            dfs(path, i+1);
            path.pop_back();
        }
    }
};

90. 子集II-(有重复-不复选-不限长)

(无重 不可复选-不限长)–给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

class Solution {
    vector<vector<int>> _res;
    vector<int> _nums;

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        _nums.assign(nums.begin(), nums.end());
        vector<int> path;
        dfs(path, 0);
        return _res;
    }

    void dfs(vector<int>& path, int start) {
        _res.push_back(path);
        // [2, 2, 3]
        for(int i = start; i < _nums.size(); i++) {   // 横向扩展
            if (i > start && _nums[i] == _nums[i-1]) {
                continue;      // 第一个是需要扩展的
            }
            path.push_back(_nums[i]);
            dfs(path, i+1);    // 纵向扩展
            path.pop_back();
        }
    }
};

77. 组合-(无重复-不复选-限长)

(无重 不可复选-限长)-- 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
2. 通过保证元素之间的相对顺序不变来 防止出现重复的组合。
3. 组合总数为 C n k C_n^k Cnk

class Solution {
    vector<vector<int>> res;
    int _n;
    int _k;
public:
    vector<vector<int>> combine(int n, int k) {
        _n = n;
        _k = k;
        vector<int> path;
        dfs(path, 0);
        return res;
    }

    void dfs(vector<int>& path, int start) {
        if (path.size() == _k) {
            res.push_back(path);
        } 
        // start 可以决定 以下代码是否执行
        for (int i = start; i < _n; i++) {
            path.push_back(i+1);
            dfs(path, i+1);
            path.pop_back();   
        }
    }
};

40. 组合总和II-(有重复-不复选-限和)

(有重复-不可复选-限和) --给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。candidates[i]>0 【减掉无端递归调用,对于减小耗时处理非常重要】
注意:解集不能包含重复的组合。

class Solution {
    vector<vector<int>> _res;
    vector<int> _nums;
    int _target;
    int _path_sum = 0;

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        _nums.assign(candidates.begin(), candidates.end());
        for (int i = 0; i < candidates.size(); i++) {
            cout << candidates[i] << ", ";
        }
        cout << endl;
        _target = target;
        vector<int> path;

        dfs(path, 0);
        return _res;
    }

    void dfs(vector<int>& path, int start) {
        // cout << "start:" << start << endl;
        if (_path_sum >= _target) {    //
            if (_path_sum == _target) {
                _res.push_back(path);
            }
            return;
        }
        for (int i = start; i < _nums.size(); i++) {
            if (i > start && _nums[i] == _nums[i-1]) {
                continue;
            }
            path.push_back(_nums[i]);
            _path_sum += _nums[i];
            dfs(path, i+1);
            _path_sum -= _nums[i];
            path.pop_back();
        }
    }
};

39. 组合总和-(无重复-可复选-限和)

(无重复-可复选-限和)–给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。

class Solution {
    vector<vector<int>> _res;
    vector<int> _nums;
    int _target;
    int _path_sum;
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        _nums.assign(candidates.begin(), candidates.end());
        _target = target;
        _path_sum = 0;
        vector<int> path;
        dfs(path, 0);
        return _res;
    }

    void dfs(vector<int>& path, int start) {
        if (_path_sum >= _target) {
            if (_path_sum == _target) {
                _res.push_back(path);
            }
            return;
        }
        for (int i = start; i < _nums.size(); i++) {
            path.push_back(_nums[i]);
            _path_sum += _nums[i];
            dfs(path, i);                // 元素可以重复选
            _path_sum -= _nums[i];
            path.pop_back();
        }
    }
};

2. 排列

46. 全排列-(无重复-不复选-不限长)

(无重复-不可复选-不限长)给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

  1. bool可选列表-保证不会重复选元素,同时能够选到 某个数字之前的数字。
class Solution {
    vector<vector<int>> _res;
    vector<bool> _used;
    vector<int> _nums;
public:
    vector<vector<int>> permute(vector<int>& nums) {
        _used.resize(nums.size());
        _nums.assign(nums.begin(), nums.end());
        vector<int> path;
        back_track(path);
        return _res;
    }

   void back_track(vector<int>& path) {
        if (path.size() == _nums.size()) {
            _res.push_back(path);
            return;
        }
        for (int i = 0; i < _used.size(); i++) {
            if (_used.at(i) == false) {
                path.push_back(_nums.at(i));
                _used.at(i) = true;
                back_track(path);
                _used.at(i) = false;
                path.pop_back();
            }
        }
    }


};

47. 全排列II-(有重复-不复选-不限长)

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

class Solution {
    vector<vector<int>> _res;
    vector<int> _nums;
    vector<bool> _used; 
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        _nums.assign(nums.begin(), nums.end());
        _used.resize(nums.size());
        vector<int> path;
        dfs(path);
        return _res;
    }

    void dfs(vector<int>& path) {
        if (path.size() == _nums.size()) {
            _res.push_back(path);
        }
        for(int i = 0; i < _nums.size(); i++) {
            if (_used[i]) {
                continue;
            }
            if (i > 0 && _nums[i] == _nums[i-1] && !_used[i-1]) {
            // 如何保证nums=[2, 2] 0_2-1_2 组成过一次,下一节点1_2-0_2 不能再次构成一个子元,否则出现重复子元
                continue; 
            }
            _used[i] = true;
            path.push_back(_nums[i]);
            dfs(path);
            path.pop_back();
            _used[i] = false;
        }
    }
};

3. 回文

// 647.回文子串(数) -- 中心扩展法(左右指针)/aux数组(暴力枚举)
int countSubstrings(string s);
// 5.最长回文子串(串) -- 中心扩展法(左右指针)/aux数组(暴力枚举)
string longestPalindrome(string s)
// 516.最长回文子序列(长) -- dp  // dp[i][j]为子串s[i:j]最长回文序列长度,短串的结果能够用于更新长串的结果,子串长度(1 -> 2 -> 3 -> n)慢慢增长,直到全串。
int longestPalindromeSubseq(string s)// 9.回文数, 234.回文链表 -- 是否
bool isPalindrome(int x);
bool isPalindrome(ListNode* head);

子串须连续,子序列无需连续。
回文子串类 :

  1. aux数组(暴力枚举) – 二维数组aux 记录s[i][j] 是否为回文子串,需要额外的空间。
  2. 中心扩展法(左右指针) – for 循环迭代中心,左右指针扩展,记录是否为回文子串,无需额外空间

647. 回文子串(数)

647.回文子串(数) – 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。子字符串是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

class Solution {
public:
	// 647.回文子串(数)-解1: 遍历所有子串,判断是否为回文子串,借助axu数组利用空间换时间
    int countSubstrings(string s) {
        int n = s.size();
        int res = 0;
        vector<vector<bool>> aux(n, vector<bool>(n, false));
        for (int i = 0; i < n; i++) {
            aux[i][i] = true;
            res++;
        }
        for (int i = n -2; i > -1; i--) {
            for (int j = i + 1; j < n; j++) {
                if (j == i + 1) {
                    aux[i][j] = (s[i] == s[j]);
                } else {
                     aux[i][j] = (s[i] == s[j] && aux[i+1][j-1]);
                }
                if (aux[i][j]) {
                    res++;
                }
            }
        }
        return res;
    }
    // 647.回文子串(数)-解2:真正中心扩展法,以 单/双字符为中心往两端扩展,每次扩展更新计数器,遇到非回文扩展即停止扩展
    //     解1 aux数组解法的剪枝版本
    int countSubstrings2(string s) {
        int n = s.size();
        int res = 0;
        for (int i = 0; i < n; i++) {
            res += palidrome_num(s, i, i);
            res += palidrome_num(s, i, i+1);
        }
        return res;
    }
    
    int palidrome_num(string s, int left, int right) {
        int n = s.size();
        int res = 0;
        while(left > -1 && right < n && s[left] == s[right]) {
            res++;
            left--;
            right++;
        }
        return res;
    }
};

5. 最长回文子串(串)

5.最长回文子串(串) – 给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

class Solution {
public:
	// 5.最长回文子串-解1: aux解
    string longestPalindrome(string s) {
        int n = s.size();
        vector<vector<bool>> aux(n, vector<bool>(n, false));
        string res;
        for (int i = 0; i < n; i ++) {
            aux[i][i] = true;
            res = s[0];
        }
        for (int i = n - 2; i > -1; i--) {
            for (int j = i + 1; j < n; j++) {
                if (j == i + 1) {
                    aux[i][j] =  (s[i] == s[j]);
                } else {
                    aux[i][j] = (s[i] == s[j] && aux[i+1][j-1]); // caution 已有条件使用
                }
                if (aux[i][j] && (res.size() < j - i + 1)) {
                    res = s.substr(i, j - i + 1);
                }
            }
        }
        return res;
    }
    
	// 5.最长回文子串-解2: 中心扩展法 
    string longestPalindrome(string s) {
        int n = s.size();
        string res;
        if (n > 0) {
            res = s[0];
        }
        for (int i = 0; i < n; i++) {
            int left = i, right = i;
            string s1 = palindrome(s, i, i);
            string s2 = palindrome(s, i, i+1);
            res = s1.size() > res.size() ? s1 : res;
            res = s2.size() > res.size() ? s2 : res; 
        }
        return res;
    }

    string palindrome(string s, int left, int right) {
        int n = s.size();
        while (left > -1 && right < n && s[left] == s[right]) {
            left--;
            right++;
        }
        return s.substr(left+1, (right - 1) - (left+ + 1) + 1); // 返回字符串,有效的left 和 right 要各自相中心缩小一格
    }
};

516. 最长回文子序列(长)dp

516.最长回文子序列 – 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
Note: 子序列无需连续, 从整体序列中抽出几个序列就行。

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));  // s[i:j] 最长回文序列长度,长度由 1 -> 2 -> 3 -> n
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }
        for (int i = n - 2; i > -1; i--) {
            for (int j = i + 1; j < n; j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i+1][j-1] + 2;   // 从对角线更新来的,长度+2
                } else {
                    dp[i][j] = max(dp[i+1][j], dp[i][j-1]);  // 从左边或者下边更新来的,选择大的那个,以备后用
                }
            }
        }
        return dp[0][n-1];

    }
};

9. 回文数、234. 回文链表(附)

9.回文数 – 给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。

234.回文链表 – 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
// way1: 保存原来的链表,翻转链表,再遍历一次链表,3N,太复杂了,
// way2: 直接存list 遍历一遍list

class Solution {
public:
	// 9.回文数, 切割数组, 12321 
        // left -> right
        // 1232 -> 1
        // 123  -> 12
        // 12 -> 123
    bool isPalindrome(int x) {

        if (x < 0 || (x > 0 && x % 10 == 0)) {
            return false;
        }
        int right_part = 0;
        while(right_part < x) {
            right_part = right_part * 10 + (x % 10);
             x = x / 10;
        }
        if (right_part == x || right_part / 10 == x) {
            return true;
        }
        return false;
    }
 
    bool isPalindrome(ListNode* head) {
        // way1: 保存原来的链表,翻转链表,再遍历一次链表,3N,太复杂了,
        // way2: 直接存list 遍历一遍list
        vector<int> val_list;
        ListNode* cur_node = head;
        while(cur_node != nullptr) {
            val_list.push_back(cur_node->val);
            cur_node = cur_node->next;
        }
        int left = 0, right = val_list.size() - 1;
        while(left < right) {
            if (val_list[left] != val_list[right]) {
                return false;
            } else {
                left++;
                right--;
            }
        }
        return true;
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值