【算法模板总结​】

全排列模板整理

基本模板

下面是47. 全排列 II的解题代码,我们将在此基础上拓展说明一般这类回溯问题的模板。

class Solution {
public:
    vector<int> cur;  // 当前可行解 
    vector<vector<int>> ret;  // 包含所有可行解的最终答案
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());  // 可有可无 视题目情况而定
        vector<bool> st(candidates.size() + 1, false); // 状态标记数组
        dfs(candidates, 0, target, st);
        return ret;
    }
    void dfs(vector<int>& candidates, int u, int target, vector<bool>& st) {
        if (target <= 0 || u >= candidates.size()) { // 默认至少有一个符合答案的组合
            if (target == 0) ret.push_back(cur);
            return;
        } 
        /* 统一for循环 */
        for (int i = u; i < candidates.size(); i++) { 
            if (i > 0 && candidates[i] == candidates[i-1] && !st[i-1])
                continue;
            /*这里如果是用for循环 是用来确定组合数的 
            如果不加for循环是用来判断子集的*/
            cur.push_back(candidates[i]);
            target -= candidates[i];
            st[i] = true;
            dfs(candidates, i + 1, target, st);
            cur.pop_back();
            target += candidates[i];
            st[i] = false;
        }
    }
};

这里问题有子集问题、有全排列问题、有组合数问题。

class Solution {
public:
    vector<int> cur;
    vector<vector<int>> ret;
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<bool> st(nums.size(), false);
        dfs(nums, 0, st);
        return ret;
    }
    void dfs(vector<int>& nums, int u, vector<bool>& st) {
        ret.push_back(cur);
        for (int i = u; i < nums.size(); i++) {
            cur.push_back(nums[i]);
            dfs(nums, i + 1, st);
            cur.pop_back();
        }
    }
};

子集问题,一开始套用的全排列模板,其思路是对于每一个数值有选或是不选两种情况。空集就是每个数都不选择的结果。但是这样行不通,所列出的结果不全。这里在dfs中应该是每执行依次dfs,首先将cur纳入最终答案ret中。每一个数只考虑一次,所以for循环的i初始值为u(已经作判断的数的个数)。子集问题和全排列问题的区别在于:子集问题每考虑一个数后都会形成一个答案;但是全排列问题是每一个数必须考虑一次后才能形成一个答案。所以是每一次执行dfs都会进行ret.push_back(cur)

class Solution {
public:
    /*
    对于一个数, 在回溯中有两种经历, 先选后回溯处于不选状态,对一个当前数x 其前面与其相同的
    数y如果处于 不选状态 说明y已经经历被选的状态 此时再选x定会重复 
    */
    vector<int> cur;
    vector<vector<int>> ans;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        cur.clear();
        ans.clear();
        sort(nums.begin(), nums.end());
        vector<bool> st(nums.size() + 1, false);
        backTrace(nums, 0, st);
        return ans;
    }
    void backTrace(vector<int>& nums, int u, vector<bool>& st) {
        ans.push_back(cur);
        for (int i = u; i < nums.size(); i++) {
            if (i > 0 && nums[i] == nums[i - 1] && st[i-1] == false) {
                continue;
            }
            cur.push_back(nums[i]);
            st[i] = true;
            backTrace(nums, i + 1, st);
            st[i] = false;
            cur.pop_back();
        }
    }
};

[子集Ⅱ]问题中有重复数值,我们要去除结果重复的子集。例如【1,1,2】。原本有两个[1,2]。我们要去除一个。在子集Ⅰ代码的基础加上一个 if 判断:
if (i > 0 && nums[i] == nums[i - 1] && st[i-1] == false)
首先,执行dfs之前数组要进行排序。这是if判断使用的前提。下面我们尝试理解一下if判断的内容。

先解释一下为什么对于当前数x,如果前一个数y=x,那么当前数就可以直接跳过。

例如[1,1,2]当遍历第一个1的时候,会把有其参与的所有子集列出来[1],[1,1],[1,2],[1,1,2]。第二个1参与的子集[1],[1,2]都已经被第一个1所包含了。所以当遇到当前数x与其前面的数y值相等的情况,就可以直接跳过x(continue)。

对于一个数,他的状态经历是false - true - false,如果当前数nums[i],其前面有一个与其数值相等。如果nums[i-1],我们就可以认为第i-1个数已经被判断过了。即他经历了false - true - false的全过程。又因为当前数x与其值相等。所以直接跳过当前数。

class Solution {
public:
    vector<vector<int>> ans;
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> st(nums.size(), false);
        vector<int> cur(nums.size(), 0);
        dfs(nums, cur, st, 0);
        return ans;
    }
    void dfs(vector<int>& nums, vector<int>& cur, vector<bool>& st, int t) {
        if (t >= nums.size()) {
            ans.push_back(cur);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (!st[i]) {
                cur[t] = nums[i];
                st[i] = true;
                dfs(nums,cur, st, t + 1); 
                st[i] = false;
            }
        }
    }
};

全排列问题代码就是经典的回溯模板,有状态数组。for循环i初始值从0开始。

class Solution {
public:
    vector<int> cur;
    vector<vector<int>> ret;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<bool> st(nums.size(), false);
        backtrace(nums, 0, st);
        return ret;
    }
    void backtrace(vector<int>& nums, int u, vector<bool>& st) {
        if (u == nums.size()) {
            ret.push_back(cur);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            
            if (st[i] || (i > 0 && nums[i-1] == nums[i] && !st[i-1])) continue;
            cur.push_back(nums[i]);
            st[i] = true;
            backtrace(nums, u + 1, st);
            cur.pop_back();
            st[i] = false;
        }
    }
};

/* 关于去重 先进行排序 如果对于当前数x 其前面与其值相同的数y y的状态历程是
false - true - false
第一个是初始状态 如果st[i-1] = false 说明y被选入组合的情况已纳入最终答案
所以当前数x不用再选入 这种情况跳过
2. 也可以这么理解
_ _ _ _ _ _ _ 假设这是数的一种组合 对于第二个位置该填入值是y 当我们面对
y后面与y值相同的数x时 他是否还要填入这个位置(第二个位置)答案是不需要 因为
原数组排序后 第二个位置前面的数都已经排好了 剩下的数的种类和个数都是固定的
即与y填入第二个位置的情况都是一样的 而y填入此位置的所有情况都已经遍历过
纳入到了最终答案中 所以当遇到x要填入这个位置时 我们就可以直接跳过这种情况。
*/

class Solution {
public:
    vector<int> cur;
    vector<vector<int>> ret;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtrace(candidates, 0, target);
        return ret;
    }
    void backtrace(vector<int>& candidates, int u, int target) {
        if (target <= 0 || u >= candidates.size()) {
            if (target == 0) ret.push_back(cur);
            return;
        }
        for (int i = u; i < candidates.size(); i++) {
            // 选
            cur.push_back(candidates[i]);
            target -= candidates[i];
            backtrace(candidates, i, target);
            // 不选
            cur.pop_back();
            target += candidates[i];
        }
    }
};

/* backtrace(candidates, u + 1, target);
这里套用的模板是子集问题中的 对于一个数 我们要么是选 要么是不选 用子集模板
在本问题中 最关键的一点是如何让一个数多次使用 在模板中 u是记录的已选数的个数
当当前数判断完后 下一次dfs理应从u+1开始判断 但是要一个数多次判断 可以让下一次
dfs中仍为u 这样就是实现了一数多判断。 妙
*/

总结

  • 子集问题1没有状态数组,子集2、全排列1、全排列2都有状态数组
  • 子集问题for循环都是从i=u开始的。 而全排列for从0开始,
  • 子集问题中for循环中的递归调用是backtrace(i + 1),表示下一次循环从第i+1个数开始遍历;全排列问题递归调用函数backtrace(u+1) 不是i,表示已经选择了u个数,该选择第u+1个数了
  • 全排列二只是在全排列1的基础上加上了去重操作,去重核心一句话:当前数和前一个数相同,并且前一个数st为false。则跳过当前数。
  • 子集问题和全排列问题去重就要在进入递归函数之前对原数组进行排序。
  • 子集问题中,每一个数有两种状态,选或者是不选。全排列问题是当前数此时选或者是不选
  • 组合数问题套用子集1模板 组合数1中每一个数可用任意次,通过backtrace(i) 来解决,不是i+1,即下一次还是从第i个数开始
  • 组合数2 套用子集2模板

名词解释

二段性: 对于一个区间,一定存在一个分界点,使得分界点两边,一边满足题目条件,一边不满足题目条件。

闰年or平年

对于非世纪年,如果能整除4就是闰年,2月有29天;对于世纪年,要能够整除400才是闰年,否则就是平年。
例如,1900年是平年,2004年是闰年
平年2月有28天
判断闰年是1、能被4整除但是不能被400整除;2、能被400整除

基本算法

在这里插入图片描述

memset

 经常看到代码这么写
 memset(dist, 0x3f, sizeof dist) 和 数组dist中的值为0x3f3f3f3f    
 这是因为memset是按字节进行初始化的, 每次更新一个字节  
 例如一个int型变量  32位  每次用3f 更新八位 更新四次 更新四次就变成了 0x3f3f3f3f 

dijkstra算法

dijkstra算法也叫做单源最短路径算法

首先这个算法是求解从源点到其余所有节点的最短路径

边权不可以为负值

在寻找路径的时候,我们可以使用倒序的方法 例如我们查找从0点到4结点的最短路径 
我们可以从4开始 向前找 直到找到0点

时间复杂度是O(n^2) 使用优先队列优化就是O(mlogn) m是边数 n是点数

例题:

在这里插入图片描述

using namespace std;const int N = 510;
 int n, m;
 int g[N][N];
 int dist[N];
 bool st[N]; // 状态数组 是否被选
 // 核心算法
 int dijkstra() {
     memset(dist, 0x3f3f3f3f, sizeof dist); // 初始化源点到所有点的距离是无穷大 
     dist[1] = 0; // 将第一个点设为源点
     
     for (int i = 1; i <= n; i++) {
         int t = -1;
         for (int j = 1; j <= n; j++) { // 遍历没被选择的所有点 寻找距离最小点
             if (!st[j] && (t == -1 || dist[t] > dist[j])) 
                 t = j;
         }
         st[t] = true;
         
         for (int j = 1; j <= n; j++ )
             dist[j] = min(dist[j], dist[t] + g[t][j]);
     }
     if (dist[n] >= 0x3f3f3f3f) return -1;
     else return dist[n];
 }
 int main()
 {
     cin >> n >> m;
     // g是邻接矩阵 
     memset(g, 0x3f3f3f3f, sizeof g);
     
     while (m--) {
         int a, b, c;
         scanf("%d%d%d", &a, &b, &c);
         g[a][b] = min(g[a][b], c);
     }
     int t = dijkstra();
     cout << t << endl;
     return 0;
 }

Floyd 算法

1. Floyd算法是求一个图中任意两点之间的最短距离

2. 边权可以为负值
   void floyd() {
         for(int k = 1; k <= n; k++)
             for(int i = 1; i <= n; i++)
                 for(int j = 1; j <= n; j++)
                     d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
     }
整个Floyd算法就是三层for循环 根据上述图片, 首先了解Floyd算法的原理,基于不断的用中间点更新邻接表
假设原图中有3个顶点,并对这些编号(0号点、 1号点 2号点…………) 首先写出初始化的临界矩阵,

在这里插入图片描述

然后用0号点去更新表格 这里用0号点更新表格的时候, 第0行、和第0列是不需要变得

在这里插入图片描述

以其中的(1,2)为例用0号点刷新就是先从1号点到达0号点 加上 从0号点到达2号点的距离 取最小值
dist[1][2] = min(dist[1][2],st[1][0] + dist[0][2])

用0号点更新完之后,下面要依次用1号点 更新 表格 这里的更新是在0号点更新之后的表格上进行的 依次更新

  void floyd() {
      for(int k = 1; k <= n; k++)
          for(int i = 1; i <= n; i++)
              for(int j = 1; j <= n; j++)
                  d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
  }
 // 可以理解为 从i到j经过前k个点 的最短距离  k是用于每次更新的点这里是按照编号的顺序进行更新的  
 // k 从1到n表示依次用所有点进行更新完  

关于自环和 存在负权值
在这里插入图片描述在这里插入图片描述

假设从a到b经过c点 原先dist[a][b] = INF dist[a][c] = INF dist[c][b] = -2 通过函数

dist[a][b] = min (dist[a][b],dist[a][c]+ dist[c][b]) dist[a][b] 被更新了 但是这是错误的 这种情况就是负权值的情况 要排除

例题

在这里插入图片描述

 using namespace std;
 const int N = 210, INF = 1e9;
 int dist[N][N];
 int n, m, k;void floyd() 
 {
     for (int k = 1; k <= n; k++) 
         for (int i = 1; i <= n; i++) 
             for (int j = 1; j <= n; j++) 
                 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
 }
 int main()
 {
     cin >> n >> m >> k;
     for (int i = 1; i <= n; i++) 
         for (int j = 1; j <= n; j++)
             if (i != j) dist[i][j] = INF;
             else dist[i][j] = 0;
             
     while (m--) {
         int x, y, z;
         scanf("%d%d%d", &x, &y, &z);
         dist[x][y] = min(dist[x][y], z);
     }
     floyd();
     
     while (k--) {
         int a, b;
         cin >> a >> b;
         if (dist[a][b] > INF / 2) puts("impossible");
         else cout << dist[a][b] << endl;
     }
     return 0;
 }

最小生成树

  • 首先 最小生成树 1、因为是树,所以结构中不能存在环 2、 所有顶点之间存在通路 要是所有顶点 3、对于一个图,它的最小生成树中边的权值之和是最小的

  • 总之, 最小生成树就是连接图中各个顶点, 并且权值和最小

Kruskal算法

Kruskal 算法 思路比较简单 就是首先将图中各个边按照权值从小到大进行排序 让后向图中进行填边
注意不能有环

例题:

在这里插入图片描述

 using namespace std;
 const int N = 200010;
 int n, m;
 int p[N];struct Edge 
 {
     int a, b, w;
     bool operator< (const Edge &W)const
     {
         return w < W.w;
     }
 }edges[N];int find(int x) 
 {
     if (p[x] != x) p[x] = find(p[x]);
     return p[x];
 }int main()
 {
     scanf("%d%d", &n, &m);
     
     for (int i = 0; i < m; i++) {
         int a, b, w;
         scanf("%d%d%d", &a, &b,&w);
         edges[i] = { a,b,w };
     }
     
     sort(edges, edges + m);
     
     for (int i = 1; i <= n; i++) p[i] = i;
     
     int res = 0, cnt = 0;
     for (int i = 0; i < m; i++) {
         int a = edges[i].a, b = edges[i].b, w = edges[i].w;
         
         a = find(a); b = find(b);
         if (a != b) {
             p[a] =b;
             res += w;
             cnt++;
         }
     }
     
     if (cnt < n - 1) puts("impossible");
     else printf("%d\n", res);
     return 0;
 }

Prim算法

prim算法将所有点分为两个集合 已选点集合, 未选点集合 , 每次从未选点集合中寻找距离 已选点集合 最近的点 并将其纳入到 已选点集合中 如此重复 直至所有点都进入已选点集合

dijkstra算法和Floyd算法都是求一个图中任意两个顶点之间的最短距离, 而prim算法和kruskal算法是求每个图的最小生成树 要求总权值最小

例题:

在这里插入图片描述

 const int N = 510, INF = 0x3f3f3f3f;
 int n, m;
 int g[N][N];
 int dist[N];
 bool st[N];int prim()
 {
     memset(dist, 0x3f, sizeof dist);
     
     int res = 0;
     for (int i = 0; i < n; i++) {
         int t = -1;
         //找dist[j] 最小的点进入集合
         for (int j = 1; j <= n; j++) 
             if(!st[j] && (t == -1 || dist[t] > dist[j]))
                 t = j;
         
         if (i && dist[t] == INF) return INF;
         
         //如果不是第一个点,就将距离加入到res中
         if (i) res +=dist[t];
         //用选定的点更新集合外的点到集合中点的距离
         for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
         
         st[t] = true;
     }
     
     return res;
 }int main()
 {
     scanf("%d%d", &n, &m);
     
     memset(g, 0x3f, sizeof g);
     
     for (int i = 0; i < m; i++) {
         int a, b, c;
         scanf("%d%d%d", &a, &b, &c);
         g[a][b] = g[b][a] = min(g[a][b], c);
     }
     
     int t = prim();
     
     if (t == INF) puts("impossible");
     else printf("%d\n", t);
     
     return 0;
 }

哈希表

字符串哈希

在这里插入图片描述

  • 字符串哈希不能将字母映射成0 如果映射成0 则(A)p 即在p进制下的A映射成0, (AA)p 也会映射成0 这样显然不可以

  • 字符串哈希过程是不允许冲突的,一般设p为131、13331.这种情况下冲突的概率几乎为0

  • 一般的哈希是允许冲突的

  • 在这里插入图片描述

  • 一般是点比较少的时候用邻接矩阵 点比较多的时候用散列表

Bellman_ford算法

  • bellman_ford算法是求解从一个固定的点经过 不超过 k条边的最短路径,也是单源最短路径
int bellman_ford() {
     memset(dist,0x3f, sizeof dist);
     dist[1] = 0;
     for (int i = 0; i < k; i++) {
         memcpy(backup, dist, sizeof dist);
         for (int j = 0; j < m; j++) {
             // 遍历所有边
             int a = edge[j].a, b = edge[j].b, w = edge[j].w;
             dist[b] = min(dist[b], backup[a] + w);
         }
     }
     return dist[n];
 }
  • 特别点 有备份数组backup

  • 这种算法的更新方式和Floyd算法的更新方式比较相像
    在这里插入图片描述

例题: 在这里插入图片描述

const int N = 510, M = 10010;
 int n, m, k;
 int dist[N], backup[N];struct Edge{
     int a, b, w;
 }edge[M];int bellman_ford() {
     memset(dist,0x3f, sizeof dist);
     dist[1] = 0;
     for (int i = 0; i < k; i++) {
         memcpy(backup, dist, sizeof dist);
         for (int j = 0; j < m; j++) {
             int a = edge[j].a, b = edge[j].b, w = edge[j].w;
             dist[b] = min(dist[b], backup[a] + w);
         }
     }
     return dist[n];
 }
 int main()
 {
     cin >> n >> m >> k;
     for (int i = 0; i < m; i++) {
         int a, b, w;
         scanf("%d%d%d", &a, &b, &w);
         edge[i] = {a,b,w};
     }
     int t = bellman_ford();
     if (t >= 0x3f3f3f3f / 2) puts("impossible");
     else 
         cout << t << endl;
     return 0;
 }

spfa算法

  1. spfa是对bellman_ford算法的优化, 但是代码像堆优化的dijkstra算法

  2. bellman_ford算法是每次都对每条边进行更新 ,但是spfa算法只对发生过更新的点 与其相连的点才更新

  3. 这是根据表达式得出的 只有dist[a] 发生变化 dist[b] = min(dist[b], dist[a] + w) 才有可能发生更改 dist[a] 发生变化是一个前提条件

例题:

在这里插入图片描述

 typedef pair<int, int> PII; //  源点到节点的距离 结点编号
 const int N = 150010;
 int n, m;
 int h[N], e[N], ne[N], w[N], idx;
 int dist[N];
 bool st[N];void add(int a, int b, int c) {
     e[idx] = b;
     w[idx] = c;
     ne[idx] = h[a];
     h[a] = idx++;
 }int spfa() {
     memset(dist, 0x3f, sizeof dist);
     dist[1] = 0;
     queue<int> q;
     q.push(1);
     st[1] = true; // 表示他在这个队列中
     
     while (q.size()) {
         int t = q.front();
         q.pop();
         
         st[t] = false;
         for (int i = h[t]; i != -1; i = ne[i]) {
             // 只更新与这个顶点相连的边
             int j = e[i];
             if (dist[j] > dist[t] + w[i])
             {
                 dist[j] = dist[t] + w[i];
                 if (!st[j]) {
                     q.push(j);
                     st[j] = true;
                 }
             }
         } 
     }
     return dist[n];
 }int main()
 {
     cin >> n >> m;
     memset(h, -1, sizeof h);
     while (m--) {
         int a, b, c;
         scanf("%d%d%d", &a, &b, &c);
         add(a, b, c);
     }
     int t = spfa();
     if (t > 0x3f3f3f3f / 2) puts("impossible");
     else cout << t << endl;
     return 0;
 }

数据结构

在这里插入图片描述

rear应该指向最后一个元素的下一个位置, 原始空间是k个 申请空间的时候要申请k+1个 空一格位置不填 是为了将判断队列空:front == rear 和队列满 rear + 1 % capacity == front 区别开来

试除法筛质数

bool isPrime(int a) {
    bool flag = true;
    for (int i = 2; i <= a / i; i++)    //注意这里是 <=sqrt(a)
        if (a % i == 0) {
            flag = false; 
            break;
        }
    return flag;
}

gcd and lcm

// 辗转相除法求解最大公约数   
 int gcd(int a, int b) {
     int c = a % b;
     while (c != 0) {
         a = b;
         b = c;
         c = a % b;
     }
     return b;
 }// 求最小公倍数 有一个数学定理 
 /*
 lcm(leatest common multiple) = a * b / gcd(a,b); 
 */ 

分解质因数


void divide(int a) {
     for (int i = 2; i <= a / i; i++) {
         if (a % i == 0) {
             int count = 0;
             while (a % i == 0) {
                 a /= i;
                 count ++;
             }
             printf("%d %d\n", i, count);
         }
     }
     // 这是处理2、3
     if (a != 1) cout << a << ' ' << 1 << endl;
     cout << endl;
 }

在这里插入图片描述

筛质数

 方案一
 

void get_primes(int n)
 {
     for (int i = 2; i <= n; i++) {
         if (!st[i]) {
             primes[cnt++] = n;
             for (int j = i + i; j <= n; j+=i) st[j] = true;
         }
     }
 }


 // 这个方法叫做埃式筛法
 时间复杂度是 O(nloglogn)

在这里插入图片描述

线性筛法在 106 时 时间复杂度和埃式筛法差不多

线性筛法

void get_primes(int n)
{
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i;
        for (int j = 0; primes[j] <= n / i; j++) {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;  走到这一步说明 primes[j] 是i的最小质因子
        }
    }
}

dfs-bfs

如果求从a到b的最短距离 那么首先用bfs思考 求最长距离 用dfs思考

区别: bfs要手写队列 代码比较长 但是bfs可以求解从a到b的最短距离 而dfs只有等所有遍历完之后才能知道从a到b的最短距离 
dfs第一次扫描到从a到b 不一定是 a到b的最短距离
dfs的代码比较短

dfs

void dfs(int x, int y) {
    for (int i = 0; i < 4; i ++) {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 0 && a < n && b >= 0 && b < m && !st[a][b] && g[a][b] == '.') {
            st[a][b] = true;
            ans++;
            dfs(a, b);
        }
    }
}


判断是否能从 a到b
ans 一开始是false  一旦从a搜索到b就 返回 不再进行搜索 
bool dfs(int ha, int la) {
    for (int i = 0; i < 4; i++) {
        int a = ha + dx[i], b = la + dy[i];
        if (a >= 0 && a < n && b >= 0 && b < n && !st[a][b] && g[a][b] == '.') 
        {
            st[a][b] = true;
            if (a == hb && b == lb) {
                return true;
            }
            ans = dfs(a,b);
        }
    }
    return ans;
}
 bfs
 ​
 void dfs(int x, int y) {
     queue<pair<int,int>> q;
     q.push({x,y});
     st[x][y] = false;
     
     while (q.size()) {
         auto it = q.front();
         q.pop();
         
         for (int i = 0; i < 4; i++) {
             int x = it.first + dx[i], y = it.second + dy[i];
             if (x >= 0 && x < n && y >= 0 && y < m && !st[x][y] && g[x][y] == '.') {
                 ans++;
                 st[x][y] = true;
                 q.push({x, y});
             }
         }
     }
 }
 !!! 上面写过 dfs不一定能求得从a到b的最短距离  实际上是可以的 通过记忆化搜索进行优化
 ​
 int dfs(int x, int y) {
     if (d[x][y] != 0) return d[x][y];
     d[x][y] = 1;
     for (int i = 0; i < 4; i++) {
         int a = x + dx[i], b = y + dy[i];
         if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] < g[x][y]) {
         // 下面这两行是关键代码 先更新 再取最大值
             dfs(a,b); // 这里是把 d[a][b] 给更新了 虽然没有什么变量把他接住
             d[x][y] = max(d[x][y], d[a][b] + 1);
         }
     }
     return d[x][y];
 }

为bfs添加一个例题

蓝桥杯 迷宫

在本题中有两个值得关注的问题

1. 题目中要求有字典序 (D < L < R < U)

2. 题目中需要记录路径

对于问题一 ,我的解决思路就是把这个字典序融入到dx,dy的那个坐标数组中,由此遍历周边位置时顺序为:下、左、右、上。这样对于本题是符合题意的,其正确性有待证明。

对于问题二,我的解决方式是:

1、定义数组last[] [],表示每个上一个点来自何方,例如如果这个点来自上方则last[x][y] = 'U' ,在定义数组last时,我们应该理清他的对应关系,例如本题中是:char dir[4] = {'U', 'R', 'L', 'D'}; 对应于dx,dy。 每次对于新加入的点,都记录他的上一位来自哪个方向

2、 利用while循环得到倒序路径 path,

3、 接着要颠倒方向即,D换成U,L换成R,R换成L,U换成D

4、 path倒序输出,因为现在是从右下角到左上角的路线, 要输出左上角到右下角的路线。

代码

 #include <iostream>
 #include <queue>
 #include <algorithm>
 #include <cstring>
 ​
 using namespace std;
 typedef pair<int,int> PII;
 const int N = 110;
 int g[N][N], d[N][N];
 char last[N][N]; 
 char dir[4] = {'U', 'R', 'L', 'D'};
 int n = 30, m = 50;
 //int n, m;
 int dx[4] = {1,0,0,-1}, dy[4] = {0,-1,1,0};
 int bfs() {
     queue<pair<int,int>> q;
     q.push({0,0});
     d[0][0] = 0; // 起点到起点的距离为0
     last[0][0] = 'E'; // 表示初始节点 
     while (q.size()) {
         PII p = q.front();
         q.pop();
         for (int i = 0; i < 4; i++) {  // D L R U
             int x = p.first + dx[i], y = p.second + dy[i];
             if (x >= 0 && x < n && y >= 0 && y < m && d[x][y] == -1 && g[x][y] == 0) {
                 d[x][y] = d[p.first][p.second] + 1;
                 q.push({x, y});
                 last[x][y] = dir[i];
             }
         }
     }
     return d[n-1][m-1];
 }
 int main()
 {//  cin >> n >> m;
 //  getchar(); // 真傻逼这个 回车 
     for (int i = 0; i < n; i++) {
         char c[50];
         gets(c);
         for (int j = 0; j < m; j++) {
             g[i][j] = c[j] - '0';
         }
     }
     
     memset(d, -1, sizeof d);
     int ans = bfs();
     
     // 倒叙寻找路径
     string path = "";
     int i = n-1, j = m-1;
     while (last[i][j] != 'E') {
         path += last[i][j];
         if (last[i][j] == 'D') i += 1;
         else if (last[i][j] == 'L') j -= 1;
         else if (last[i][j] == 'R') j += 1;
         else if (last[i][j] == 'U') i -= 1;
     } 
     // 反向对应字符串
     for (int i = 0; i < path.size(); i++) {
         if (path[i] == 'D') path[i] = 'U'; 
         else if (path[i] == 'U') path[i] = 'D'; 
         else if (path[i] == 'R') path[i] = 'L'; 
         else if (path[i] == 'L') path[i] = 'R'; 
     } 
     string ret = ""; 
     for (int i = path.size() - 1; i>= 0; i--) ret += path[i]; 
     cout << ret << endl;
     return 0;
 }


二分


       int i = 0, j = n - 1;
       // 寻找左边界
       while (i < j) {
           int mid = i + j >> 1;
           if (a[mid] >= k) j = mid;
           else i = mid + 1;
       }
       if (a[i] != k) cout << "-1 -1" << endl;
       else {
           cout << i << ' ';
           i = 0, j = n - 1;
           // 寻找右边界
           while (i < j) {
               int mid = i + j + 1 >> 1;
               if (a[mid] <= k) i = mid;
               else j = mid - 1;
           }
           cout << i << endl;
       }

       
数的范围查找分为两次, 寻找左边界 寻找右边界  
   寻找左边界 第一次变得是 r  判断是>=  mid取左不加一   左r>=不加一
   寻找右边界 第一次变得是 l  判断是<=  mid取右需加一   右l<=需加一

王道上有一道题,关于二分:线性表(a1,a2,a3…,an)中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用时最少时间在表中查找数值为x的元素,若找到,则将其与后继元素位置相交换,若找不到,则将其插入表中并使表中元素仍递增有序。

/* 1.这道题如果用上面求左边界的模板也是可以的,最终l==r,并且如果A[i] != k,
 r指向的是有序列表中第一个大于k的数。
 2.不行,上述有误,例如【1,2,5,6,9】,如果k=10,采用寻找左边界的算法,
 那么r最后指向的是9,并不是大于k的数。那么,你又会想,可否用l来表示(推测:l指向
 的应该是第一个小于k的数),不行,例如k=0,此时l指向的是1,不是第一个小于k数。
 综上所述:面对这一类问题还是用答案上的代码比较合理;
*/
int low = 0, high = n-1, mid;
while (low <= high) {
	mid = (low + high) / 2;
	if (A[mid] == k) break;
	else if (A[mid] < k) low = mid + 1;
	else high = mid - 1; 
	}
	if (A[mid] == k && mid != n-1) {
		t = A[mid]; A[mid] = A[mid+1]; A[mid+1] = t;
	}
	if (low > high) {
		// 插入这个数
		for (int i = n-1; i > high; i--) A[i+1] = A[i];
		A[n-1] = k;
	}
}
这个算法和求左右边界算法区别就是while循环条件是low<=high,即当low==high的
时候,算法还会执行一次,这就说明如果序列中不存在k,那么hign指向的是第一个小于
k的数。如果插入的话,那么A[hign]=k.
其次区别就是l = mid + 1; r = mid - 1; l、r等价变化。

差分

一维差分

题目:

在这里插入图片描述

 #include <iostream>
 #include <algorithm>
 ​
 using namespace std;
 const int N = 100010;
 int a[N], b[N];
 int n, m;int add(int l, int r, int c) {
     b[l] += c;
     b[r + 1] -= c;
 }
 ​
 ​
 int main()
 {
     scanf("%d%d", &n, &m);
     for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
     // 差分法是对数组某个区间进行整体相加减的操作, 差分要申请两个数组,一个是原数组,一个是差分数组b。
     // 差分模板记住
     // 求原数组的差分数组
     // !!! b是a的差分数组,那么a就是b的前缀和数组
     for (int i = 1; i <= n; i++) b[i] = a[i] - a[i - 1];
     
     while (m--) {
         int l, r, c;
         scanf("%d%d%d", &l, &r, &c);
         add(l, r, c);
     }
     
     // 将b数组重新转换成前缀和数组
     for (int i = 1; i <= n; i++) {
         b[i] += b[i - 1];
         printf("%d ", b[i]);
     }
 }



二维差分

在这里插入图片描述


#include <iostream>
 ​
 using namespace std;const int N=1010;
 int n,m,q;
 int a[N][N],b[N][N];void insert(int x1,int y1,int x2,int y2,int c){
     b[x1][y1]+=c;
     b[x2+1][y1]-=c;
     b[x1][y2+1]-=c;
     b[x2+1][y2+1]+=c;
 }
 int main()
 {
     scanf("%d%d%d",&n,&m,&q);
     
     for(int i=1;i<=n;i++)
         for(int j=1;j<=m;j++)
         {
             scanf("%d",&a[i][j]);
             insert(i,j,i,j,a[i][j]);
         }
     while(q--){
         int x1, y1, x2, y2,c;
         cin>>x1>>y1>>x2>>y2>>c;
         insert( x1, y1, x2, y2,c);
     }
     
     for(int i=1;i<=n;i++)
         {for(int j=1;j<=m;j++){
             b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];
             cout<<b[i][j]<<" ";
         }
             cout<<endl;
         }
 }

日期计算模板

* 这是蓝桥被 跑步训练

* 这种情况是两边都包括的天数,例如从五号到八号,是5 6 7 8是四天。
 #include <iostream>
 #include <queue>
 ​
 using namespace std;
 int a[2][13] = {{0,31,29,31,30,31,30,31,31,30,31,30,31}, {0,31,28,31,30,31,30,31,31,30,31,30,31}};
 int main()
 {
     int y = 2000, m = 1, d = 1;
     int tmp = 1;
     int week = 6;
     int ans = 1;
     while (y != 2020 || m != 10 || d != 1) {
         tmp++;
         d ++;
         // 天数是否合理
         // 首先判断 闰年还是平年
         int f = ((y % 4 == 0 && y % 400 != 0) || (y % 400 == 0)) ? 0 : 1;
         if (d > a[f][m]) {
             d = 1;
             m ++;
         } 
         // 判断月份是否合理
         if (m > 12) {
             m = 1;
             y++;
         } 
         week ++;
         if (week == 8) {
             week = 1; 
         }
         if (week == 1 || d == 1) ans ++; // 周一和月第一天 多训练 
     } 
     cout << tmp << endl;
     cout << tmp + ans << endl;
 }

动态规划

dp问题的所有优化都是对代码的等价变形

步骤:

「状态定义」

「状态转移方程」

「初始化」

「输出」

「是否可以空间优化」
  1. 01背包

    有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

    // 未优化前
     for (int i = 1; i <= n; i++) {
             for (int j = 0; j <= m; j++) {
                f[i][j] = f[i - 1][j];    // 不选第i个物品 
                if (j >= v[i]) {  // 选第i个物品 
                    f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); 
                }
             }
     }
     ​
     ​
     // 优化后 
     for (int i = 1; i <= n; i++) {
            cin >> v >> w;        // 这里是空间优化 由此就不用申请数组了
            for (int j = m; j >= v; j--) {  //从大到小枚举空间,就可以将 二维f数组变为一维
                f[j] = max(f[j], f[j - v] + w);
            }
        }
  1. 完全背包

    • 有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
    // 原始代码
     for (int i = 1; i <= n; i++) {
         for (int j = 0; j <= m; j++) {
             for (int k = 0; k * v[i] <= j; k++) {
                 f[i][j] = max(f[i-1][j], f[i-1][j-k*v[i]] + k * w[i]);
             }
         }
     }


  

    这里是三层for循环,当数据量为1000时就会超时。所以肯定要优化。
     这里看一下表达式:
         f[i][j] = max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2*v]+2*w...f[i-1][j-k*v]+w*k)
         f[i][j-v] = max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w...)
         可以看到f[i][j] = max(f[i-1][j],f[i][j-v]+w)
         所以这就是优化的过程
         so..
         01背包:f[i][j] = max(f[i-1][j],f[i-1][j-v]+w)
         完全背包:f[i][j] = max(f[i-1][j],f[i][j-v]+w)
     ​
     for(int i = 1; i <= n; i++)
         for (int j = 1; j <= m; j++) {
             f[i][j] = f[i-1][j]; // 不选第i个物品
             if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i]);
             // 没见物品有无限件  关键在于怎么理解这么写 一件物品就会一直装 直到装不下 原因在上面解释了
         }
         
     /*
     对于 完全背包 和 01背包 未优化的代码  对于循环中的语句
     f[i][j] = f[i-1][j] 无论j是从v[i] 到 m   还是从m 到 v[i] 变成一维数组之后都是 f[j] = f[j] 由于计算是先算等号右边的  计算f[j] 时  “本层”的f[j] 还没有算出来 用的都是上一层的f[i-1][j]
     */
     ​
     for (int i = 1; i <= n; i++) 
             for (int j = v[i]; j <= m; j++) {
                 f[j] = max(f[j], f[j - v[i]] + w[i]);
             }


         
  1. 多重背包Ⅰ
  • 有 N 种物品和一个容量是 V 的背包。

  • 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

    // 朴素代码
     for (int i = 1; i <= n; i++) {
         for (int j = 0; j <= m; j++) {
             for (int k = 0; k <= s[i] && v[i]*k <= j;k++)
                 f[i][j] = max(f[i][j], f[i-1][j-k*v[i]] + k*w[i]);
         }
     }
     // 优化后
     for (int i = 1; i <= n; i++) {
             int v, w, s;
             cin >> v >> w >> s;
             for (int j = m; j >= v; j--) {
                 for (int k = 1; k <= s && k * v <= j; k++) 
                     f[j] = max(f[j], f[j - k * v] + k * w);
             }
     }
  1. 多重背包Ⅱ
  • 有 N 种物品和一个容量是 V 的背包。

  • 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。多重背包Ⅱ得数据范围更大 c++语言每秒钟可以计算107~108 之间

 多重背包Ⅱ相当于对多重背包Ⅰ得再次优化
 设 s = 1023
 我们没必要一一列举 0、1、2、3……1023 
 可以将其分组 1、2、4、8、……512  2的整数幂
 例如7 我们可以用1、2、4  将0~7得数都列举出来
 1: 1
 2: 2
 3: 1 2
 4: 4
 5: 1 4
 6: 2 4
 7: 1 2 4
 ​
 10 可以分成1、2、4、3  最后一个3是 10 - 1-2-4 = 3 计算出来得  不能是8 因为 1+2+4+8=15>10  会列举出不需要得数
 因为1\2\4 可以列举出 0~7 那么 每个数都加上3 会出现3~10  所以1、2、4、3 可以列举出10以内得所有数
 ​
 以此为原理 可以将第i个物品得s个 分解 不必要一一列举
  

for (int i = 1; i <= n; i++) {
         int v, w, s;
         cin >> v >> w >> s;
         for (int k = 1; k <= s; k *= 2) {
             s -= k;
             goods.push_back({k * v, k * w}); // 存储体积 / 价值
         }
         if (s > 0) 
             goods.push_back({s * v, s * w});
     }
     
     for (auto good : goods) {
         for (int j = m; j >= good.v; j--) {
             f[j] = max(f[j], f[j - good.v] + good.w);
         }
     } 

整数划分

method1

## 方程
    f[i][j]=f[i-1][j] + f[i-1][j-i] + f[i-1][j-2*i] + ... + f[i-1][j-n*i]
    f[i][j-i]=          f[i-1][j-i] + f[i-1][j-2*i] + ... + f[i-1][j-n*i]
    
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];

int main()
{
    cin >> n;
    f[0] = 1; // 初始化也很重要
    for (int i = 1; i <= n; i++) {
        for (int j = i; j <= n; j++) {
            f[j] = (f[j] + f[j-i]) % mod;
        }
    }
    cout << f[n] << endl;
    return 0;
}

method2

在这里插入图片描述

#include <iostream>

using namespace std;

const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];

int main()
{
    cin >> n;
    f[0][0] = 1;  // 初始化  和为0 个数为0 也是一种方案
    
    for (int i = 1; i <= n; i++) {
        // 和为i  个数为j  j不可能超过i  最多的情况是i个1
        for (int j = 1; j <= i; j++) {
            f[i][j] = (f[i-1][j-1] + f[i-j][j]) % mod;
        }
    }
    
    int res = 0;
    for (int i = 1; i <= n; i++) res = (res + f[n][i]) % mod;
    cout << res << endl;
    return 0;
    

回溯

46. 全排列

这是简单的一个模型
在这里插入图片描述

void dfs(vector<int>& nums, vector<int>& cur, vector<bool>& st, int t) {
        if (t >= nums.size()) {
            ans.push_back(cur);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (!st[i]) {
                cur[t] = nums[i];
                st[i] = true;
                dfs(nums,cur, st, t + 1); 
                st[i] = false;
            }
        }
    }

上升子序列区别

最长上升子序列和最大上升子序列的代码十分相似,其主要区别就是判断完a[i] > a[j]后 f[i]的取值

1.对于最长上升子序列 f[i] = max (f[i],f[j] + 1) 他是长度 和a[i]的值无关

2.对于最大上升子序列 f[i] = max (f[i], f[j] + a[i]) 他和a[i]的值有关

3.都是两层循环
最长上升子序列
    

f[0] = 1;
    int ans = f[0];
    for (int i = 1; i < n; i++) {
        f[i] = 1;
        for (int j = 0; j < i; j++) {
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
            else if (a[i] == a[j]) f[i] = max(f[i], f[j]); 
        }
        ans = max(ans, f[i]);
    }


    
最大上升子序列
    
    

f[0] = a[0];
    int ans = f[0]; 
    for (int i = 1; i < n; i++) {
        f[i] = a[i];
        for (int j = 0; j < i; j++) {
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + a[i]);
        }
        ans = max(ans, f[i]);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值