Algorithm

本文详细介绍了多种算法和数据结构,包括动态规划问题的优化方法,如01背包、完全背包和股票问题的解法,以及二分查找、最小生成树的计算,如Prim和Kruskal算法。此外,还讲解了字符串哈希、回溯法以及Floyd和Dijkstra算法的应用。并查集和差分的运用也在文中有所阐述。
摘要由CSDN通过智能技术生成
  • 目前的基本宗旨是:牢固基础只是,任何操作都要记录,因为不记录过一段时间脑子就会忘记。

名词解释

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

闰年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 算法

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

  • 边权可以为负值

  • 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算法

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

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

这是根据表达式得出的 只有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<=需加一

差分

一维差分

题目:

 #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问题的所有优化都是对代码的等价变形

步骤

  • 「状态定义」

  • 「状态转移方程」

  • 「初始化」

  • 「输出」

  • 「是否可以空间优化」

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);
            }
        }

完全背包

  • 有 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]);
             }
         
      

多重背包Ⅰ

  • 有 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);
             }
     }

多重背包Ⅱ

有 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]的取值

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

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

  • 都是两层循环

最长上升子序列
    
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]);
    }

Leetcode

代码模板块

// 第一种方式
	int p = nums[0], r = 0, ans = p;
        for (int i = 1; i < nums.size(); i++) {
            r = max(p + nums[i], nums[i]);
            ans = max(ans, r); // ans记录最大值
            p = r;
        }
        return ans;
    // 第二种方式
        int imax = 0, Ansmax = nums[0];
        for (int i = 0; i < nums.size(); i++) {
            imax = max(imax + nums[i], nums[i]);
            Ansmax = max(Ansmax, imax);
        }
        return Ansmax;
        
    // 同时记录最大值和最小值的方式
    int imax = 1, imin = 1, Ansmax = nums[0];
        for (int i = 0; i < nums.size(); i++) {
            // 如果是负数 就交换最大值和最小值
            // 把最大值变成最大的 那个 负数 这样与nusm[i] 相乘之后就又会变成最大的
            // 同理 最小值变成最大的那个 正数 这样与nums[i] 相乘之后就又会变成最小的
            if (nums[i] < 0) {
                int tmp = imax;
                imax = imin;
                imin = tmp;
            }
            imax = max(imax * nums[i], nums[i]); // 2 
            imin = min(imin * nums[i], nums[i]); // 1
            Ansmax = max(imax, Ansmax);            // 2
        } 
        return Ansmax;
class Solution {
public:
	// 这是一种思路
    int pos[100010], neg[100010];
    int getMaxLen(vector<int>& nums) {    
        // pos[i] 表示以i结尾的最长连续子串  正值
        // neg[i] 表示以i结尾的最长连续子串  负值
		// 原始代码
        // int ans = 0;
        // for (int i = 1; i <= nums.size(); i++) {
        //     if (nums[i - 1] > 0) {
        //         pos[i] = pos[i-1] + 1;
        //         // 这里用 ? 是因为 如果neg[i] = 0 那么加上nums[i]不能变成负数 
        //         // 如果不为零 则一个负数乘以一个正数还是一个负数
        //         neg[i] = neg[i - 1] ? neg[i - 1] + 1 : 0;
        //     } else if (nums[i - 1] < 0) {
        //         pos[i] = neg[i-1] ? neg[i - 1] + 1 : 0; // 这里原因同上
        //         neg[i] = pos[i-1] + 1;
        //     } else {
        //         pos[i] = 0;
        //         neg[i] = 0;
        //     }
        //     ans = max(ans, pos[i]);
        // }
        // return ans;
		
		// 优化
        int ans = 0, ipos = 0, ineg = 0;
        for (int i = 1; i <= nums.size(); i++) {
            if (nums[i - 1] > 0) {
                // pos[i] = pos[i-1] + 1;
                ipos = ipos + 1;
                // neg[i] = neg[i - 1] ? neg[i - 1] + 1 : 0;
                ineg = ineg ? ineg + 1 : 0; 
            } else if (nums[i - 1] < 0) {
                // pos[i] = neg[i-1] ? neg[i - 1] + 1 : 0; // 这里原因同上
                int tmp = ipos;
                ipos = ineg ? ineg + 1 : 0;
                // neg[i] = pos[i-1] + 1;
                ineg = tmp + 1;
            } else {
                ipos = 0;
                ineg = 0;
            }
            ans = max(ans, ipos);
        }
        return ans;
    }
};

899. 有序队列

对于这个问题, 当k=1时 对于给定得字符串 每次都是将头字母转移到尾部 这样得到得结果都是唯一得 所以只需要遍历、求解那个字母在最前面时得字典序最小

当k > 1 时 对于任意得字符串 经过若干次得操作 都肯定能把它变成字典有序的 例如 “andfd" 当k>2时 最终会变成”addfn“

class Solution {
public:
    string orderlyQueue(string s, int k) {
        if (k==1) {
            string smallest = s;
            int length = s.size();
            for (int i = 0; i < length; i++) {
                char a = s[0];
                s = s.substr(1);
                s.push_back(a);
                if (s < smallest) smallest = s;
            }
            return smallest;
        }
        else {
            sort(s.begin(), s.end());
            return s;
        }
    }
};

918. 环形子数组的最大和

【解答】:

这是环形数组最大和

一个很棒的思路是这样的:

如图 我们求最大的子段和 这个最大的子段和分布有两种情况 1、就是普通型的 最大和 等于 非环形数组的的 最大和

2、就是在数组 头一部分 尾一部分 数组的中间有部分不取 那么这种情况我们可以用下面这种方法 替代

就是求 非环形数组中间的最小值 用数组总和减去最小值就可以得出此时的最大值

那么这两种方法就可以等价为 非环形数组 求两遍

第一遍是求普通的数组最大子段和 maxAns

第二种是求普通的数组最小子段和 minAns

然后比较 ans = max(all - minAns, maxAns);

注意 此时应该注意另一种特殊情况 : 当数组中全为负数的时候

当全为负数的时候 求得的minAns = all 又因为此时maxAns 就为数组中值最大的那个值

所以在最后返回结果是返回的是 all-minAns = 0; 但是世界上应该返回maxAns 即数组中值最大的那个

这种情况另外讨论

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        int maxp = nums[0],minp = nums[0], maxr = 0, minr = 0;
        int maxAns = nums[0], minAns = nums[0], all = nums[0];
        for (int i = 1; i < nums.size(); i++) {
            // 这是以nums[i]结尾的最大子段和
            maxr = max(maxp + nums[i], nums[i]);
            maxAns = max(maxr, maxAns);
            maxp = maxr;
            
            // 这是以nums[i]结尾的最小子段和
            minr = min(minp + nums[i], nums[i]);
            minAns = min(minr, minAns);
            minp = minr;

            all += nums[i];
        }
        // 处理全是负数的情况 
        if (all == minAns) return maxAns;
        return max(maxAns, all - minAns);
        // 如果只有一个元素 那么最小字段和 就是这个元素
        // int p = nums[0], r = 0, ans = nums[0];
        // for (int i = 1; i < nums.size(); i++) {
        //     // 如果f[i-1] >= 0 那么f[i]=nums[i] 因为加上f[i-1]不会让f[i]变小
        //     // 如果f[i-1] < 0 此时 f[i] = f[i-1] + nums[i] 
        //     // 综上 f[i] = min(f[i-1] + nums[i], nums[i]);
        //     r = min(p + nums[i], nums[i]);
        //     ans = min(r, ans);
        //     p = r;
        // }
        // return ans;
    }
};

761. 特殊的二进制序列

首先说明什么样的串才是最大的 :【就是让1尽量多的在前面】 同时要注意题目中可以进行交换操作的是两个 连续 并且都是 特殊 子串的串才能进行交换

对于 符合题意 得特殊序列,其中得头部得‘1’和尾部得‘0‘的零是不能通过任意次操作得到的、如果能得到,就说明这个字符串不是 特殊 序列 证明如下:

例如有个串为 不可再分割的子串 (这只是假设)【101100y】将头部的【10】和 后面的【1100】交换

  • 0的数量等于1的数量

  • 二进制序列的每一个前缀码中1的数量大于等于0的数量

由此,可以知道y也肯定是 特殊 子串 因为它符合 这两个要求 所以这个串就可以再分出一个 特殊 的子串 所以这个大串 并不是 特殊 的串 矛盾

所以 任何一个 特殊 的串的头’1‘ 和尾部的’0‘都是不能通过交换得到的

class Solution {
public:
    string makeLargestSpecial(string s) {
        if (s.size() <= 2) {
            return s;
        }
        // cnt 是记录1和0的数量差 如果为0 就说明遇到了一个01相等的串 才能进一步操作
        // left 记录的是从 子串的起始下标
        int cnt = 0, left = 0; 
        vector<string> subs;
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == '1') {
                cnt++;
            } else {
                cnt--;
                if (cnt == 0) {
                	// 这里的递归要理解好
                	// 它是如果判断了这个子串的 1 和 0 数量相等 那么就当他是 特殊 子串
                	// 去掉 头部 1 和尾部 0 
                    subs.push_back('1' + makeLargestSpecial(s.substr(left+1,i-left-1)) + '0');
                    left = i + 1;
                }
            }
        }
        sort(subs.begin(), subs.end(), greater<string>{});
        string ans = accumulate(subs.begin(), subs.end(), ""s);
        return ans;
    }
};   000,000,000

股票问题

121. 买卖股票的最佳时机

// 这个是最基本的
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // 记录 第i个元素之前的最小值 那么第i天的最大利润就是 prices[i] - pre_max;
        int pre_min = 10001, ans = -1;

        for (int i = 0; i < prices.size(); i++) {
            ans = max(ans, prices[i] - pre_min);
            pre_min = min(pre_min, prices[i]);
        }
        if (ans < 0) return 0;
        return ans;
    }
};

122. 买卖股票的最佳时机 II

这道题可以用动态规划解答,也可以用贪心解答 : 记录这道题是因为题解中有一个很好的点睛之笔。

这个题解真是一针见血、令人醍醐灌顶 这就是贪心的思路 :

由于题目中说可以当天买入卖出 所以:

可以这么理解 只要有钱赚 我们就赚 在代码上体现就是只要今天比昨天的股票贵 即有钱赚 我们就卖出

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int ans = 0;
        for (int i = 1; i < prices.size(); i++) {
            int diff = prices[i] - prices[i-1];
            if (diff > 0) ans += diff; 
        }
        return ans;
    }
};

769. 最多能完成排序的块

class Solution {
public:
    int maxChunksToSorted(vector<int>& arr) {
        // 这个问题的很好的一个思路
        // 如果前i个物品的最大值是Max == i  说明该位置可以分块  
        // 如果前i个物品的最大值 max!= i 则不可以分块
        // 如果前i个物品的最大值max > i 则后面一定有小于max的值 
        // 要把这个值与max划分到一个组中
        int ans = 0, curMax = 0;
        for (int i = 0; i < arr.size(); i++) {
            curMax = max(curMax, arr[i]);
            if (curMax == i) ans++;
        }
        return ans;
    }
};

768. 最多能完成排序的块 II

class Solution {
public:
    int maxChunksToSorted(vector<int>& arr) {
    // 这种思路就是 有一个备份数组 将原数组备份一下 然后将原数组进行排序 
    // 然后遍历排序前后的数组 sum1 和 sum2  
    // 当遍历到第i个的时候 sum1 == sum2 时 说明 这前i个元素可以分为一个组 
    // 这个思路很巧妙
        vector<int> backups = arr;
        sort(arr.begin(), arr.end());
        long sum1 = 0, sum2 = 0, num = 0;
        for (int i = 0; i < arr.size(); i++) {
            sum1 += arr[i];
            sum2 += backups[i];
            if (sum1 == sum2) num++;
        }
        return num;
    }
};
总结: 最多能完成排序的块 1和2 这两个题 第一个数组内容有明确的规定 是从 0~n  对于这种情况 也可以用第二种方法做 但是第一种方法效率更高 他是直接检测前i个数的最大值 是否等于i 如果等于i 说明对前i个物品进行分组排序后的顺序和原数组前i个元素排序后是一样的
  第二种方法更加好理解, 对原数组进行备份 backup 然后对原数组进行排序 之后遍历两个数组 求前i个数的和 如果sum1 == sum2 说明这前i个数组进行排序后和原数组进行排序后是一样的 
  这种问题同时下次做的时候 要注意他和数组元素下标相关 排序问题要想着数组下标问题

1302. 层数最深叶子节点的和--按层遍历代码模板

/**
 * Definition for a binary tree node.
 * 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 Solution {
public:
    int deepestLeavesSum(TreeNode* root) {
        // 按层遍历
        queue<TreeNode*> q;
        // vector<TreeNode*> vt;
        int sum = 0;
        q.push(root);
        q.push(nullptr); // 这里是一层结束的标记

        while (true) {
            // vt.clear();
            sum = 0;
            auto it = q.front();
            q.pop();
            while (it != nullptr) {
                // vt.push_back(it);
                sum += it->val;
                if (it->left != nullptr) q.push(it->left);
                if (it->right != nullptr) q.push(it->right); 
                it = q.front();
                q.pop();     
            }
            if (q.size() != 0)  q.push(nullptr);
            else break;
        }
        // int ans = 0;
        // for (auto it : vt) {
        //     ans += it->val;
        // }
        // return ans;
        return sum;   
    }
};

滑动窗口

76. 最小覆盖子串

模板

class Solution {
public:
    string minWindow(string s, string t) {
        int sLen = s.size();
        int tLen = t.size();
        if (!sLen || !tLen || sLen < tLen) return "";
        
        int winFreq[128] = {0}, tFreq[128] = {0};
        for (char c : t) tFreq[c]++;
        int distance = 0;
        int minLen = sLen + 1;
        int begin = 0;
        int l = 0, r = 0;
        // 左闭右开
        while (r < sLen) {
            if (tFreq[s[r]] == 0) {
                r++;
                continue;
            }
            if (winFreq[s[r]] < tFreq[s[r]]) distance++;

            winFreq[s[r]]++;
            r++;

            // tLen 不是下标 所以这里不是tLen-1
            while (distance == tLen) {
                if (r - l < minLen) {
                    minLen = r - l;
                    begin = l;
                }
                if (tFreq[s[l]] == 0) {
                    l++;
                    continue;
                }
                if (tFreq[s[l]] == winFreq[s[l]]) distance--;
                winFreq[s[l]]--;
                l++;
            }
        }
        if (minLen == sLen + 1) return "";
        else return s.substr(begin,minLen);
    }
};

子集、全排列问题总结

  • 子集问题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模板

洛谷

挖地雷

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
bool g[21][21]; // g[i][j] = true 表示这两个地窖联通
bool st[21]; // st[i] 为true 表示这个地窖 前面有地窖与之联通
int p[21]; // 表示 地雷数量
int n;
int ans = 0, cur = 0;
int curpath[21], path[21], respath[21];
// respath记录的是最终结果  

 void dfs(int root, int CurAns) {
	// 将地窖变成树  以没有前缀的第i个地窖开始 
	// 对于每一条从根节点到叶子节点的路径 我们都要记录一下 进入该路径时的ans 以及出该路径的cur
	for (int i = root + 1; i < n; i++) {
		cur = CurAns; 
		if (g[root][i]) {
			cur += p[i];
			curpath[i] = 1;
			dfs(i,cur);
			if (cur > ans) {
				ans = cur;
				memcpy(path, curpath, sizeof path);
			}
			curpath[i] = 0; // 恢复 这是路径需要
		}
	}
}

int main()
{
	cin >> n;
	for (int i = 0; i < n; i++) scanf("%d", &p[i]);
	for (int i = 0; i <= n - 1; i++) {

		for (int j = i + 1; j < n; j++) {
			scanf("%d", &g[i][j]);
			if (g[i][j] == 1) st[j] = true;
		}
	}
    int res = 0;
	for (int i = 0; i < n; i++) {
		if (!st[i]) { 
		// 这一步对整个算法都很关键
		// 没有前缀 这一步是一个优化  只有没有前缀的才能进行路径计算
		// 例如 如果从1号点到2号点有通路 那么我们从1号点开始挖的地雷数肯定比第二号点开始挖的地雷数多
		// 所以只要有前缀的 就不用考虑 这里的前缀指的是前面有与之相连的地窖
			memset(curpath, 0, sizeof curpath);
			memset(path, 0, sizeof path);
			curpath[i] = 1;
			path[i] = 1;
			ans = p[i];
			dfs(i, ans);
			if (res < ans) {
				res = ans;
				memcpy(respath, path, sizeof path);
			}
		}
	}
	for (int i = 0; i < n; i++) if (respath[i]) cout << i + 1 << ' ';
	cout << endl;
	cout << res << endl;
	return 0;
}

数据结构

一句话总结

  • 按层遍历用到的数据结构是队列 不是栈

二叉树

  • 这里规定书的根节点处于第一层

  1. 二叉树树中,第 i 层最多有2i-1 个结点

  2. 如果二叉树的深度是k , 那么二叉树最多有2k-1 个结点

  3. 二叉树中, 中断结点(叶子节点树)为n0 则n0 = n2 + 1.

  • 对于性质三中的解释  
    设总结点数n 度为0、1、2的结点数分别是 n0、n1、n2
    则n = n0 + n1 + n2
    假设一个树中所有分枝数是B。每个分支对应一个结点 所以n = B + 1; 这个1 表示的是根节点
    则n = B + 1;  又因为 B = n1 + n2 * 2
    所以 n = n0 + n1 + n2 = n1 + n2 * 2 + 1;
    n0 = n2 + 1

Trie 树

用处:高效的存储和查找字符串集合的数据结构

  • 每个单词的结尾元素都要有一个标记 意思是这个单词以这个字母结束

  • 使用trie树做的题目, 题意中肯定声明了是全大写字母 或者 是全小写字母 不会有很多

代码模板
算法竞赛进阶指南--代码模板
int trie[SIZE][26], tot = 1;
void insert(char* str) {
	int len = strlen(str), p = 1;
	for (int k = 0; k < len; k ++) {
		int ch = str[k] - 'a';
		if (trie[p][ch] == 0) trie[p][ch] = ++tot;
		p = trie[p][ch];
	}
	end[p] = true; // 这是在字符串尾部做标记
}

bool search(char* str) {
	int len = strlen(str), p = 1;
	for (int k = 0; k < len; k++) {
		p = trie[p][str[k] - 'a'];
		if (p == 0) return false; // 如果查找的某个字符不存在 那么直接返回0
	}
	// 走到这说明每个字符都存在 只需判断是否是字符结尾就行
	return end[p];
}


const int N = 100010;
int son[N][26]; // 每个结点最多有26个子节点
int cnt[N]; // 表示以(i  + 'a')对应字母结尾的字母的个数 
// 这里cnt[N] 大小要设置成N cnt[p]++ 这里记录的是结尾字母的下标 并不是26个英文字母 不能写成26 
int idx; // 下标是0的点,既是根节点,又是空结点
char str[N]; //读入的字符串
// 插入
void insert(char str[]) {
    int p = 0;
    for (int i = 0; str[i]; i++) {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++idx;
        // son[][] 记录的是下标
        p = son[p][u];
    }
    cnt[p]++; // 例如插入 "ab" "ab" "ab" 此时以b结尾的就有三个
}
// 查询
int query(char str[]) {
    int p = 0;
    for (int i = 0; str[i]; i++) {
        int u = str[i] - 'a';
        // 如果没有从p到u的线 
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

并查集

  • 注意事项

    • (并查集只能用于无向图, 二传递闭包可以用于无向图和有向图)

    • 并查集可以用来维护许多额外信息

    • 【在同一棵树上时 说明两个点的关系已经确定了】

用途:

  • 询问两个元素是否在同一个集合中

  • 合并两个集合

    • 并查集中会记录每个集合的长度 绑定到根节点上

    • 同时记录每个结点到根节点的距离 绑定到每个节点上 事实上往往是根据这一点初始化并查集

    • 初始化 : for (int i = 0; i < n; i++) p[i] = i;

  • 基本原理:每个集合用一颗树表示。树根的编号就是每个集合的编号。每个结点存储它的父结点, p[x]表示x的父节点

  • 问题1:如何判断树根: if(p[x] ==x)

  • 问题2:如何求x的集合编号:while (p[x] != x) x = p[x]

  • 问题3:如何合并两个结合:px是x集合的编号,py是y集合的编号 p[x] = y 就是将一个集合变为另一个集合的孩子

  • 优化: 路径压缩方法

    • 按秩合并

  • 代码:

  • int n, m;
    int p[N];
    
    int find(int x) {
        // 这一步 返回了x的祖宗结点 同时又实现了路径压缩
        // 将x寻找祖宗结点这一路上的点的父结点都指向根节点
        // 如果x不是根节点 那么x的父结点指向x的根节点
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    // 维护每个点到根节点距离的代码 
    int find(int x) {
        if (p[x] != x) {
            int t = find(p[x]);
            d[x] += d[p[x]];  // 这是一个累加过程 操作完之后d[x] 表示x到根节点的距离
            p[x] = t;        
        }    
        return p[x];
    }

离散化

离散化是将一些将大但很稀疏的数 映射到较小的范围

常用的实现方法

  • 如果是无序的 , 即不要求有序 那么我们可以用哈希表进行实现 代码如下:

  • // 离散化用到的是哈希表	
    unordered_map<int,int> S;
    
    int get(int x) {
        if (S.count(x) == 0) S[x] = ++n;
        return S[x];
    }
  • 还有一种是要求有序的,

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值