目录
一,消除重复
1,程序的组成结构
程序可以细分为4个代码层次:数据+数据类型+基础运算+算法
PS:int、float、vector<int>、vector<float>、节点类型是int的二叉树、节点类型是float的二叉树就是6种不同的数据类型,更复杂的数据结构,也叫数据类型。
2,变化的层次
如果有2段代码包含重复的知识,那就需要消除重复。
消除重复,就是将软件中的变和不变分离,再重新组织在⼀起。
按照程序的层次,重复可以分为三个层次:数据变化、类型变化、行为变化。
这3个变化层次,分别对应前3个代码层次,如果最高层次的算法不一样,那就不属于重复代码,而是无关代码了。
3,重复场景、消除手段、产物和条件
(1)数据变化
场景:2段代码的数据不同,数据类型和行为相同
消除重复的手段:数据参数化(提取函数)
产物:普通函数
编译条件:无
运行条件:数据在某个范围内
PS:这个数据范围可能等于该数据类型的全体范围,下同。
PS:数据在范围A内,函数可以正常运行结束,数据在范围B内,功能符合预期,B可能等于A也可能是A的真子集
(2)类型变化
场景:2段代码的数据和数据类型不同,行为相同
消除重复的手段:类型参数化(模板编程)
产物:泛型函数
编译条件:类型参数满足运算条件
运行条件:数据在某个范围内
PS:运算条件指的是该类型具有可能存在的一元运算和二元运算,下同。
(3)行为变化
场景:2段代码的数据、数据类型和行为都不同,但算法是相同的
消除重复的手段:行为数据化,进一步参数化(函数指针)
产物:泛型算法(带函数指针的泛型函数)
编译条件:类型参数满足运算条件,函数指针满足赋值运算条件
运行条件:数据在某个范围内,函数指针满足关系条件
PS:函数指针满足对应的关系条件,才能使得功能符合预期。这个关系可能包括二元关系、交换律、结合律等。
4,条件类型实例
(1)数据在某个范围内
int f(int x)
{
return 10 / x;
}
条件:范围是x!=0
int f(int x)
{
if (x == 0)return 0;
return 10 / x;
}
条件:范围是全体int数(无条件)
(2)类型参数满足一元运算
template<typename T>
T opt(T a)
{
return !a;
}
条件:T类型满足一元位运算非运算,假设运算结果是T2类型,那么T类型还满足一元运算赋值运算,从T2到T赋值。
PS:即使是从T到T的赋值,也不是所有类型都满足的。
template<typename T>
int opt(T a)
{
return a.x;
}
条件:T类型满足,具有成员x
(3)类型参数满足二元运算
template<typename T>
int opt(T a, T b)
{
return a + b;
}
条件:T类型满足加法运算
template<typename T>
int opt(T a)
{
return a > T{ 0 };
}
条件:T类型满足用0构造这个一元运算,还满足比较大小这个二元运算,即序关系。
PS:所有的二元关系,都可以理解成满足二元运算。
(4)函数指针满足赋值运算
template<typename A>
A f(A a, int n, A(*pfunc)(A, A, int))
{
A x = pfunc(a, a, n);
A y = pfunc(x, x, n);
return y;
}
条件:A类型满足赋值运算
template<typename A, typename P>
A f(A a, int n, P pfunc)
{
A x = pfunc(a, a, n);
A y = pfunc(x, x, n);
return y;
}
条件:A类型满足赋值运算,且A类型可以隐式转换成P的前2个参数类型,且int类型可以隐式转换成P的最后一个参数类型。
(5)函数指针满足关系条件
class SemiGroup
{
public:
//枚举只去掉1个数(v.size()>1),剩下的数做p累积运算的结果
template<typename T, typename Tfunc>
static vector<T> allExceptOne(vector<T>& v, Tfunc p) {
vector<T>left(v.size() - 1), right(v.size() - 1);
T x = v[0];
for (int i = 1; i < v.size(); i++)left[i - 1] = x, x = p(x, v[i]);
x = v.back();
for (int i = int(v.size()) - 2; i >= 0; i--)right[i] = x, x = p(v[i], x);
vector<T>ans(1, right[0]);
for (int i = 0; i < left.size() - 1; i++)ans.push_back(p(left[i], right[i + 1]));
ans.push_back(left.back());
return ans;
}
};
条件:T类型上的p运算满足半群。
二,重构实例
1,数据变化
原代码:
void func()
{
int a, b, c;
int d = a * b + c * c * c;
// ......
int e, f, g;
int h = e * f + g * g * g;
}
重构代码:
int opt(int a, int b, int c)
{
return a * b + c * c * c;
}
void func()
{
int a, b, c;
int d = opt(a, b, c);
// ......
int e, f, g;
int h = opt(e, f, g);
}
2,类型变化
原代码:
int opt(int a, int b, int c)
{
return a * b + c * c * c;
}
int opt(float a, float b, float c)
{
return a * b + c * c * c;
}
这个代码已经完成了第一层次的消除重复,还需要继续消除重复。
重构代码:
template<typename T>
int opt(T a, T b, T c)
{
return a * b + c * c * c;
}
模板编程的更多内容,参考C++模板编程
3,行为变化
原代码:
template<typename A, typename N>
static inline A multiAdd(A a, N n, int p)
{
if (n <= 1)return a;
A ans = multiAdd<A, N>(a, n / 2, p);
ans = (ans + ans) % p;
if (n % 2)ans = (ans + a) % p;
return ans;
}
template<typename A, typename N>
static inline A multiMulti(A a, N n, int p)
{
if (n <= 1)return a;
A ans = multiMulti<A, N>(a, n / 2, p);
ans = (ans * ans) % p;
if (n % 2)ans = (ans * a) % p;
return ans;
}
这个代码已经完成了第一层次和第二层次的消除重复,还需要继续消除重复。
重构代码:
template<typename A, typename N>
static inline A aijiMulti(A a, N n, int p, A(*pfunc)(A, A, int))
{
if (n <= 1)return a;
A ans = aijiMulti<A, N>(a, n / 2, p, pfunc);
ans = pfunc(ans, ans, p);
if (n % 2)ans = pfunc(ans, a, p);
return ans;
}
template<typename A>
static inline A opAdd(A x, A y, int p)
{
return (x + y) % p;
}
template<typename A>
static inline A opMulti(A x, A y, int p)
{
return (x * y) % p;
}
template<typename A, typename N>
static inline A multiAdd(A a, N n, int p)
{
return aijiMulti(a, n, p, opAdd<A>);
}
template<typename A, typename N>
static inline A multiMulti(A a, N n, int p)
{
return aijiMulti(a, n, p, opMulti<A>);
}
PS:这里给出的是数据和数据类型都有变化的场景,如果是数据有变化但是数据类型没有变化的场景,那就是基于普通函数(非目标函数)做同样的重构。
三,ACM模板
1,vector<T> + 等价关系
C ++版本
//从vector(一维或2维)中删除所有的x
template<typename T>
static void deletAllX(vector<T>& v, T x)
{
for (int i = 0; i < v.size(); i++)if (v[i] == x)v.erase(v.begin() + i--);
}
template<typename T>
static void deletAllX(vector<vector<T>>& v, T x)
{
for (auto& vi : v)deletAllX(vi, x);
}
//判断数组去掉相邻重复数之后有多少个不同的数
template<typename T>//T可以是指针或迭代器,统计范围是[left,right)
static int getDifNum(T left, T right)
{
if (left == right)return 0;
int ans = 1;
for (T it = left + 1; it != right; it++)ans += *it != *(it - 1);
return ans;
}
template<typename T>
static int getDifNum(vector<T>v)
{
return getDifNum(v.begin(), v.end());
}
2,vector<T> + 全序关系
//vector的择优合并
template<typename T>
static vector<T> mergeVector(const vector<T>& a, const vector<T>& b)
{
vector<T> ans;
int i;
for (i = 0; i < a.size() && i < b.size(); i++) {
if (isGreater(a[i], b[i]))ans.push_back(a[i]);
else ans.push_back(b[i]);
}
for (; i < a.size(); i++)ans.push_back(a[i]);
for (; i < b.size(); i++)ans.push_back(b[i]);
return ans;
}
template<typename T>
static bool isGreater(T a, T b)
{
return a > b;
}
3,vector<T> + 字典序
//vector的字典序比较,v1<v2是true,v1>=v2是false
template<typename T>
static bool cmpVector(const vector<T>& v1, const vector<T>& v2)
{
for (int i = 0; i < v1.size() && i < v2.size(); i++)
{
if (v1[i] != v2[i])return v1[i] < v2[i];
}
return v1.size() < v2.size();
}
//vector的字典序排序
template<typename T>
static void sortVector(vector<vector<T>>& v)
{
sort(v.begin(), v.end(), cmpVector<T>);
}
4,vector<T> + 半群
这里我限制了vectro长度至少为2,所以需要的是半群。
如果限制改成长度至少为1,则需要传入单位元,即需要的是幺半群。
//vector+半群
class VecSemiGroup
{
public:
template<typename T>
static vector<T> allExceptOneMin(vector<T>& v)
{
return allExceptOne(v, [](T a, T b) {return min(a, b); });
}
template<typename T>
static vector<T> allExceptOneOr(vector<T>& v)
{
return allExceptOne(v, [](T a, T b) {return a | b; });
}
private:
//枚举只去掉1个数(v.size()>1),剩下的数做p累积运算的结果
template<typename T, typename Tfunc>
static vector<T> allExceptOne(vector<T>& v, Tfunc p) {
vector<T>left(v.size() - 1), right(v.size() - 1);
T x = v[0];
for (int i = 1; i < v.size(); i++)left[i - 1] = x, x = p(x, v[i]);
x = v.back();
for (int i = int(v.size()) - 2; i >= 0; i--)right[i] = x, x = p(v[i], x);
vector<T>ans(1, right[0]);
for (int i = 0; i < left.size() - 1; i++)ans.push_back(p(left[i], right[i + 1]));
ans.push_back(left.back());
return ans;
}
};
值得深思的是,我们说这个一个基于半群的泛型算法,其实说的是“如果p是一个满足半群的运算,那么allExceptOne求出来的就是枚举只去掉1个数,剩下的数做p累积运算的结果”,如果p不满足半群,那么allExceptOne求出来的就是枚举只去掉1个数,剩下的左边的数按左结合做p累积运算,右边的数按右结合做p累积运算,最终左右两边做p运算得到的结果。
也就是说,即使p不满足半群,这仍然是一个算法。力扣 1554. 只有一个不同字符的字符串 用的就是这个算法。
p不满足半群的情况下,求出来的结果的用法很有限,不像allExceptOneMin求出来的结果的数值有具体用法,力扣 1554. 只有一个不同字符的字符串中的p是一个哈希函数,allExceptOne求出来的结果仅仅用于比较2个结果是否相等。所以说,p不满足半群的用法属于额外的拓展用法,正常情况下p应该满足半群。
力扣 265. 粉刷房子 II
假如有一排房子共有 n 幢,每个房子可以被粉刷成 k 种颜色中的一种。房子粉刷成不同颜色的花费成本也是不同的。你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
每个房子粉刷成不同颜色的花费以一个 n x k 的矩阵表示。
例如,costs[0][0] 表示第 0 幢房子粉刷成 0 号颜色的成本;costs[1][2] 表示第 1 幢房子粉刷成 2 号颜色的成本,以此类推。
返回 粉刷完所有房子的最低成本 。
示例 1:
输入: costs = [[1,5,3],[2,9,4]]
输出: 5
解释:
将房子 0 刷成 0 号颜色,房子 1 刷成 2 号颜色。花费: 1 + 4 = 5;
或者将 房子 0 刷成 2 号颜色,房子 1 刷成 0 号颜色。花费: 3 + 2 = 5.
示例 2:
输入: costs = [[1,3],[2,4]]
输出: 5
提示:
costs.length == n
costs[i].length == k
1 <= n <= 100
2 <= k <= 20
1 <= costs[i][j] <= 20
进阶:您能否在 O(nk) 的时间复杂度下解决此问题?
class Solution {
public:
int minCostII(vector<vector<int>>& costs) {
vector<int> ans = costs[0];
for (int i = 1; i < costs.size(); i++) {
vector<int>t = costs[i];
auto min2 = AllExceptOneMin(ans);
for (int i = 0; i < t.size(); i++)t[i] += min2[i];
ans = t;
}
return *min_element(ans.begin(), ans.end());
}
};
力扣 2003. 每棵子树内缺失的最小基因值(min)
有一棵根节点为 0
的 家族树 ,总共包含 n
个节点,节点编号为 0
到 n - 1
。给你一个下标从 0 开始的整数数组 parents
,其中 parents[i]
是节点 i
的父节点。由于节点 0
是 根 ,所以 parents[0] == -1
。
总共有 105
个基因值,每个基因值都用 闭区间 [1, 105]
中的一个整数表示。给你一个下标从 0 开始的整数数组 nums
,其中 nums[i]
是节点 i
的基因值,且基因值 互不相同 。
请你返回一个数组 ans
,长度为 n
,其中 ans[i]
是以节点 i
为根的子树内 缺失 的 最小 基因值。
节点 x
为根的 子树 包含节点 x
和它所有的 后代 节点。
示例 1:
输入:parents = [-1,0,0,2], nums = [1,2,3,4] 输出:[5,1,1,1] 解释:每个子树答案计算结果如下: - 0:子树包含节点 [0,1,2,3] ,基因值分别为 [1,2,3,4] 。5 是缺失的最小基因值。 - 1:子树只包含节点 1 ,基因值为 2 。1 是缺失的最小基因值。 - 2:子树包含节点 [2,3] ,基因值分别为 [3,4] 。1 是缺失的最小基因值。 - 3:子树只包含节点 3 ,基因值为 4 。1是缺失的最小基因值。
示例 2:
输入:parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3] 输出:[7,1,1,4,2,1] 解释:每个子树答案计算结果如下: - 0:子树内包含节点 [0,1,2,3,4,5] ,基因值分别为 [5,4,6,2,1,3] 。7 是缺失的最小基因值。 - 1:子树内包含节点 [1,2] ,基因值分别为 [4,6] 。 1 是缺失的最小基因值。 - 2:子树内只包含节点 2 ,基因值为 6 。1 是缺失的最小基因值。 - 3:子树内包含节点 [3,4,5] ,基因值分别为 [2,1,3] 。4 是缺失的最小基因值。 - 4:子树内只包含节点 4 ,基因值为 1 。2 是缺失的最小基因值。 - 5:子树内只包含节点 5 ,基因值为 3 。1 是缺失的最小基因值。
示例 3:
输入:parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8] 输出:[1,1,1,1,1,1,1] 解释:所有子树都缺失基因值 1 。
提示:
n == parents.length == nums.length
2 <= n <= 105
- 对于
i != 0
,满足0 <= parents[i] <= n - 1
parents[0] == -1
parents
表示一棵合法的树。1 <= nums[i] <= 105
nums[i]
互不相同。
思路:
2次dfs
第一次dfs,把每颗子树的所有节点的最小值算出来。
第二次dfs,根据父节点的最小缺失值,算出所有子节点的最小缺失值。
具体来说,子节点的最小缺失值是3个值的最小值,这3个分别是父节点的最小缺失值、父节点的值、所有兄弟节点及其子节点的最小值。
class Solution {
public:
vector<int> smallestMissingValueSubtree(vector<int>& parents, vector<int>& nums) {
int n = parents.size();
sons.resize(n);
mins.resize(n);
ans.resize(n);
for (int i = 1; i < parents.size(); i++)sons[parents[i]].push_back(i);
getMin(nums, 0);
map<int, int>m;
for (auto x : nums)m[x]++;
int minMiss = 1;
while (m[minMiss])minMiss++;
dfs(nums, 0, minMiss);
return ans;
}
private:
int getMin(vector<int>& nums, int id) {
int ans = nums[id];
for (auto son : sons[id])ans = min(ans, getMin(nums, son));
return mins[id] = ans;
}
void dfs(vector<int>& nums, int id, int miss) {
ans[id] = miss;
miss = min(miss, nums[id]);
if (sons[id].size() <= 1) {
for (auto son : sons[id])dfs(nums, son, miss);
return;
}
vector<int>sonMin;
for (auto son : sons[id])sonMin.push_back(mins[son]);
auto otherMin = AllExceptOneMin(sonMin);
for (int i = 0; i < sons[id].size(); i++) {
dfs(nums, sons[id][i], min(miss, otherMin[i]));
}
}
private:
vector<vector<int>>sons;
vector<int>mins;
vector<int>ans;
};
力扣 2680. 最大或值(或)
给你一个下标从 0 开始长度为 n
的整数数组 nums
和一个整数 k
。每一次操作中,你可以选择一个数并将它乘 2
。
你最多可以进行 k
次操作,请你返回 nums[0] | nums[1] | ... | nums[n - 1]
的最大值。
a | b
表示两个整数 a
和 b
的 按位或 运算。
示例 1:
输入:nums = [12,9], k = 1 输出:30 解释:如果我们对下标为 1 的元素进行操作,新的数组为 [12,18] 。此时得到最优答案为 12 和 18 的按位或运算的结果,也就是 30 。
示例 2:
输入:nums = [8,1,2], k = 2 输出:35 解释:如果我们对下标 0 处的元素进行操作,得到新数组 [32,1,2] 。此时得到最优答案为 32|1|2 = 35 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= k <= 15
思路:
k次操作一定会用完,而且一定会加在同一个数上,所以只需要枚举除了这个数之外,其他的数的或运算总结果。
或运算是环非域,但是这里只需要用到半群的结合性。
class Solution {
public:
long long maximumOr(vector<int>& nums, int k) {
if (nums.size() == 1)return nums[0] << k;
auto x = AllExceptOneOr(nums);
long long ans = 0;
for (int i = 0; i < x.size(); i++)ans = max(ans, ((long long)nums[i] << k) | x[i]);
return ans;
}
};
力扣 1554. 只有一个不同字符的字符串
给定一个字符串列表 dict
,其中所有字符串的长度都相同。
当存在两个字符串在相同索引处只有一个字符不同时,返回 True
,否则返回 False
。
示例 1:
输入:dict = ["abcd","acbd", "aacd"] 输出:true 解释:字符串 "abcd" 和 "aacd" 只在索引 1 处有一个不同的字符。
示例 2:
输入:dict = ["ab","cd","yz"] 输出:false
示例 3:
输入:dict = ["abcd","cccc","abyd","abab"] 输出:true
提示:
dict
中的字符数小于或等于10^5
。dict[i].length == dict[j].length
dict[i]
是互不相同的。dict[i]
只包含小写英文字母。
进阶:你可以以 O(n*m)
的复杂度解决问题吗?其中 n 是列表 dict
的长度,m
是字符串的长度。
思路:
枚举一个字符串去掉任意一个字符之后的哈希值。
即使自定义的哈希运算不满足结合律,也仍然可以利用半群模板达到效果,且性能达标。
long long myhash(long long x, long long y)
{
return (x * x % 1000000007 * 97 + y * 11 + 5) % 1000000007;
}
class Solution:VecSemiGroup {
public:
bool differByOne(vector<string>& dict) {
vector<set<long long>>vs(dict[0].length());
for (auto& s : dict) {
auto v = hashs(s);
for (int i = 0; i < vs.size(); i++) {
if (vs[i].find(v[i]) != vs[i].end())return true;
vs[i].insert(v[i]);
}
}
return false;
}
vector<long long> hashs(string& s) {
vector<long long>v(s.length());
for (int i = 0; i < s.length(); i++)v[i] = s[i] - 'a';
return allExceptOne(v, myhash);
}
};
5,树 + 半群、幺半群
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class TreeSemiGroup
{
public:
static map<TreeNode*, int> allNodeSum(TreeNode *root) {
return allNodeMultiOptP(root, [](int a, int b) {return a + b; });
}
protected:
//枚举以任意节点作为根得到的树,该树所有节点按照中序做p累积运算的结果
template<typename Tfunc>
static map<TreeNode*, int> allNodeMultiOptP(TreeNode *root, const Tfunc &p) {
map<TreeNode*, int> ans;
if (!root)return ans;
dfs(root, p, ans);
return ans;
}
private:
template<typename Tfunc>
static int dfs(TreeNode *root, const Tfunc &p, map<TreeNode*, int>&m)
{
int ans = root->val;
if (root->left)ans = p(dfs(root->left, p, m), ans);
if (root->right)ans = p(ans, dfs(root->right, p, m));
return m[root] = ans;
}
};
class TreeMonoid:public TreeSemiGroup
{
public:
//枚举去掉1个节点和它的所有子节点,剩下的树按照中序做p累积运算的结果
template<typename Tfunc>
static map<TreeNode*, int> allExceptOne(TreeNode *root, Tfunc p, int e) {
map<TreeNode*, int> allMulti = allNodeMultiOptP(root, p);
map<TreeNode*, int> ans;
dfs(root, p, e, e, allMulti, ans); // e是单位元素
return ans;
}
private:
template<typename Tfunc>
static void dfs(TreeNode *root, Tfunc p, int faLeft, int faRight, map<TreeNode*, int> &allMulti, map<TreeNode*, int> &ans) {
ans[root] = p(faLeft, faRight);
if (root->left) {
int ans = root->val;
if (root->right)ans = p(ans, allMulti[root->right]);
dfs(root->left, p, faLeft, p(ans, faRight), allMulti, ans);
}
if (root->right) {
int ans = root->val;
if (root->left)ans = p(allMulti[root->left], ans);
dfs(root->right, p, p(faLeft, ans), faRight, allMulti, ans);
}
}
};