关于c++的几个简单算法 & csp-J必会算法详解

 一. 动态规划(Dynamic Programming)

难点:状态转移方程的构建和初始化条件的设计
典型问题:01背包问题
分析
状态定义 dp[i][j] 表示前i个物品放入容量为j的背包的最大价值。状态转移需要判断是否选择当前物品。

#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int n, capacity;
    cin >> n >> capacity;
    int weight[n], value[n];
    int dp[n+1][capacity+1] = {0}; // 初始化为0

    for (int i = 0; i < n; i++) 
        cin >> weight[i] >> value[i];

    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= capacity; j++) {
            if (j >= weight[i-1])  // 能装下当前物品
                dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i-1]] + value[i-1]);
            else 
                dp[i][j] = dp[i-1][j]; // 不装当前物品
        }
    }
    cout << dp[n][capacity];
    return 0;
}

关键点

  • 状态转移方程:max(不选当前物品,选当前物品)

  • 时间复杂度:O(n * capacity),需注意数据规模是否允许。

1. 矩阵取数游戏(P1005)

  • 题目类型:区间DP + 高精度

  • 难点:需要处理大数运算(__int128或高精度类),状态转移涉及从两端取数的最优解。

  • 状态定义dp[i][j] 表示区间 [i, j] 取数的最大得分。

  • 转移方程

    dp[i][j] = max(dp[i+1][j] * 2 + a[i], dp[i][j-1] * 2 + a[j]);
  • 代码参考洛谷P1005题解


2. 石子合并(P1880)

  • 题目类型:区间DP + 环形处理

  • 难点:环形问题需展开为链式(复制数组),并枚举分割点 k

  • 状态定义dp[i][j] 表示合并区间 [i, j] 的最小/最大代价。

  • 转移方程

dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum[i][j]);

3. 过河卒(P1002)

  • 题目类型:坐标DP + 路径计数

  • 难点:处理马的控制点(不可达位置),初始化边界条件。

  • 状态定义dp[i][j] 表示到达 (i, j) 的路径数。

  • 转移方程

    dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 非马控制点


4. 滑雪(P1434)

  • 题目类型:记忆化搜索(DFS + DP)

  • 难点:需递归遍历四个方向,记忆化存储已计算的结果。

  • 状态定义dp[x][y] 表示从 (x, y) 出发的最长滑坡长度。

  • 转移方程

    dp[x][y] = max(dfs(nx, ny) + 1); // (nx, ny) 为合法邻接点
  • 代码参考动态规划与记忆化搜索题单


5. 关路灯(P1220)

  • 题目类型:区间DP + 状态附加维度

  • 难点:需记录当前区间和位置(左/右端点),分类讨论移动方向。

  • 状态定义dp[i][j][0/1] 表示关闭 [i, j] 区间的灯后位于左/右端点的最小功耗。

  • 转移方程

    dp[i][j][0] = min(dp[i+1][j][0] + cost1, dp[i+1][j][1] + cost2);
  • 代码参考洛谷区间DP题单


6. 合唱队形(P3205)

  • 题目类型:区间DP + 双端插入

  • 难点:需记录最后插入的元素是左端还是右端。

  • 状态定义dp[i][j][0/1] 表示区间 [i, j] 以左/右端结尾的排列方案数。

  • 转移方程

    if (a[i] < a[i+1]) dp[i][j][0] += dp[i+1][j][0];
  • 代码参考洛谷区间DP题单

例题说明 :

  • 入门题:过河卒(P1002)、数字三角形(P1216)。

  • 进阶题:石子合并(P1880)、关路灯(P1220)。

  • 高难度题:矩阵取数(P1005)、滑雪(P1434)。


二. 图论 - 最短路径(Dijkstra算法)

难点:优先队列优化和松弛操作的理解
代码示例

#include <vector>
#include <queue>
using namespace std;

const int INF = 0x3f3f3f3f;
vector<pair<int, int>> graph[1001]; // 邻接表:graph[u] = {v, weight}
int dist[1001]; // 最短距离数组

void dijkstra(int start) {
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
    fill(dist, dist + 1001, INF);
    dist[start] = 0;
    pq.push({0, start});

    while (!pq.empty()) {
        int u = pq.top().second;
        int d = pq.top().first;
        pq.pop();
        if (d > dist[u]) continue; // 已找到更优路径,跳过

        for (auto &edge : graph[u]) {
            int v = edge.first, w = edge.second;
            if (dist[v] > dist[u] + w) { // 松弛操作
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}

关键点

  • 使用优先队列优化,每次取出距离最小的点。

  • 松弛操作:若通过当前点u到达v更近,则更新dist[v]

  • 注意:Dijkstra不能处理负权边!

1. P4779 【模板】单源最短路径(标准版)

  • 题目描述:给定有向图,求从源点出发到所有点的最短路径。

  • 算法:Dijkstra堆优化(优先队列实现)。

  • 关键点

    • 使用邻接表存图。

    • 优先队列优化,时间复杂度为O((V+E)logV)。

    • 注意不能在更新时判断vis,而应在出队时判断。

  • 代码参考:见洛谷P4779题解


2. P3371 【模板】单源最短路径(弱化版)

  • 题目描述:与P4779类似,但数据范围较小,适合练习基础Dijkstra。

  • 算法:Dijkstra(未优化或优先队列优化)。

  • 关键点

    • 弱化版允许使用未优化的Dijkstra(O(V²))。

    • 输出要求中,不可达点输出2³¹-158。

  • 代码参考:见洛谷P3371题解


3. P1339 Heat Wave G

  • 题目描述:无向图,求从起点到终点的最短路。

  • 算法:Dijkstra或SPFA(题目未禁用SPFA时)。

  • 关键点

    • 使用邻接矩阵存图(适合稠密图)。

    • 注意无向图的边需双向处理。

  • 代码参考:见洛谷P1339题解


4. P1629 邮递员送信

  • 题目描述:求从源点到所有点的最短路及所有点返回源点的最短路之和。

  • 算法:Dijkstra + 反向建图。

  • 关键点

    • 正向图跑一次Dijkstra,反向图再跑一次。

    • 反向图可将“返回路径”转化为单源最短路问题。

  • 代码参考:见洛谷题单图论2-2


5. P2296 寻找道路

  • 题目描述:在满足路径点出边均与终点连通的条件下,求最短路径。

  • 算法:拓扑排序 + Dijkstra/BFS。

  • 关键点

    • 先用反向图拓扑排序筛选合法点。

    • 再在合法点上跑最短路6。

  • 代码参考:见洛谷P2296题解


6. P1144 最短路计数

  • 题目描述:求从起点到各点的最短路径条数(边权为1)。

  • 算法:BFS或Dijkstra(带DP统计)。

  • 关键点

    • dis[y] == dis[x]+1,则累加路径数。

    • 需模100003输出4。

  • 代码参考:见洛谷题单图论2-2


题目编号名称算法难度
P4779单源最短路径(标准版)Dijkstra堆优化普及+/提高
P3371单源最短路径(弱化版)Dijkstra基础普及-
P1339Heat Wave GDijkstra/SPFA普及
P1629邮递员送信Dijkstra + 反向图普及+/提高
P2296寻找道路拓扑排序 + 最短路提高+/省选-
P1144最短路计数BFS/Dijkstra + DP普及+/提高

例题说明: 建议按顺序练习,从模板题(P3371、P4779)开始,逐步挑战综合应用题(如P2296)。更多题目可参考洛谷题单图论2-2


三. 深度优先搜索(DFS)与剪枝

难点:剪枝条件的合理设计
典型问题:全排列问题
代码示例

#include <iostream>
using namespace std;

int n, path[10];
bool visited[10];

void dfs(int step) {
    if (step == n) { // 终止条件
        for (int i = 0; i < n; i++) 
            cout << path[i] << " ";
        cout << endl;
        return;
    }

    for (int i = 1; i <= n; i++) {
        if (!visited[i]) { // 剪枝:已用过的数字不再选
            visited[i] = true;
            path[step] = i;
            dfs(step + 1);
            visited[i] = false; // 回溯
        }
    }
}

int main() {
    cin >> n;
    dfs(0);
    return 0;
}

关键点

  • 使用visited数组避免重复选择,实现剪枝。

  • 回溯时恢复状态(visited[i] = false)。

1、全排列类问题

例题1. ​P1255 数楼梯(改)​
  • 问题描述:计算走n阶楼梯的方案数,每次可走1或2阶。
  • DFS实现:递推公式 f(n) = f(n-1) + f(n-2),边界条件 f(0)=f(1)=1
  • 剪枝技巧:直接递归的时间复杂度为O(2ⁿ),但本题数据范围较小(n ≤ 10),无需剪枝。优化方法可采用记忆化搜索或递推法,将复杂度降至O(n)。
例题2. ​火星人排列问题​(类似洛谷题目)
  • 问题描述:给定一个排列,求其后的第r个字典序排列。
  • DFS实现:通过回溯生成全排列,记录当前排列顺序。
  • 剪枝技巧:找到目标排列后,标记并立即剪枝后续所有分支,避免无效搜索

2、组合优化问题

例题3. ​P2036 [COCI2008-2009#2] PERKET
  • 问题描述:选择若干调料,最小化总酸度与总苦度的绝对差。
  • DFS实现:枚举每种调料的“选”与“不选”,记录当前酸度积和苦度和。
  • 剪枝技巧
    • 可行性剪枝:必须至少选一种调料,否则结果无效。
    • 最优性剪枝:若当前酸度积与苦度差的绝对值已超过已找到的最小值,则提前终止该分支。
例题4. ​P1433 吃奶酪
  • 问题描述:在平面上找到吃掉所有奶酪的最短路径。
  • DFS实现:枚举所有可能的路径顺序,计算总距离。
  • 剪枝技巧
    • 最优性剪枝:实时记录当前最短路径长度,若某次搜索的累积距离已超过该值,立即回溯。
    • 状态缓存:通过标记已访问的奶酪避免重复计算。

3、迷宫与路径问题

例题5. ​P1605 迷宫
  • 问题描述:从起点到终点的可行路径数,存在障碍物。
  • DFS实现:向四个方向递归搜索,标记已访问的位置。
  • 剪枝技巧
    • 边界检查:超出迷宫范围或遇到障碍物时终止分支。
    • 状态回溯:递归返回后恢复当前位置的未访问状态,避免路径重复。
例题6. ​P1443 马的遍历
  • 问题描述:计算马从起点到棋盘各点的最少步数。
  • DFS/BFS实现:通常用BFS更高效,但DFS可通过剪枝优化。
  • 剪枝技巧:若当前步数已超过已知到达某点的最短步数,终止该分支。

4、经典剪枝策略总结

  1. 可行性剪枝:提前判断当前路径是否可能满足条件(如越界、不合法状态)。
    • 示例:迷宫问题中遇到障碍物立即终止搜索。
  2. 最优性剪枝:在求最优解的问题中,若当前路径的代价已超过已知最优解,提前回溯。
    • 示例:吃奶酪问题中实时更新最短路径并剪枝。
  3. 状态去重:通过哈希或标记数组避免重复访问同一状态。
    • 示例:全排列问题中通过vis数组标记已选元素。
  4. 搜索顺序优化:优先搜索更可能接近答案的分支,减少无效计算。
    • 示例:PERKET问题中按字典序优先选择调料组合。

四. 并查集(Union-Find)

难点:路径压缩和按秩合并的优化
代码示例

int parent[1001];
int rank[1001];

void init() {
    for (int i = 0; i < 1001; i++) {
        parent[i] = i;
        rank[i] = 1;
    }
}

int find(int x) { // 路径压缩
    if (parent[x] != x)
        parent[x] = find(parent[x]);
    return parent[x];
}

void union_set(int x, int y) { // 按秩合并
    int rootx = find(x), rooty = find(y);
    if (rootx == rooty) return;
    if (rank[rootx] > rank[rooty]) 
        parent[rooty] = rootx;
    else {
        parent[rootx] = rooty;
        if (rank[rootx] == rank[rooty]) 
            rank[rooty]++;
    }
}

关键点

  • find函数通过递归实现路径压缩,降低树高。

  • union_set根据秩的大小合并,避免树退化。

1、基础并查集模板题

例题1. ​P1551 亲戚 
  • 问题描述:判断多对人物是否属于同一家族(即是否连通)。
  • 实现思路
    • 初始化并查集,每个节点的父节点指向自己。
    • 合并所有输入的亲戚关系。
    • 对每个查询使用 find 操作判断两个节点是否属于同一集合。
  • 关键优化
    • 路径压缩:在 find 函数中通过递归或循环将节点直接连接到根节点,降低树的高度。
    • 时间复杂度:单次操作接近 O(1)。
  • 代码要点
    int find(int x) {
        if (father[x] == x) return x;
        return father[x] = find(father[x]); // 路径压缩
    }
例题2. ​P3367 【模板】并查集 
  • 问题描述:实现并查集的合并与查询操作。
  • 实现思路
    • 操作类型为 1 时合并两个集合,为 2 时查询两个元素是否同属一个集合。
  • 代码特点
    • 与 P1551 类似,但输入格式包含动态操作指令,适合作为并查集的标准模板题。

2、并查集扩展应用

例题3. ​P1892 [BOI2003] 团伙 
  • 问题描述:处理朋友和敌人两种关系,朋友的朋友是朋友,敌人的敌人也是朋友。
  • 实现思路
    • 扩展域并查集:将每个元素 i 拆分为两个域:朋友域 i 和敌人域 i+n。
    • 合并逻辑:
      • 若 a 和 b 是朋友,合并 a 和 b。
      • 若 a 和 b 是敌人,合并 a 与 b+n、b 与 a+n。
  • 关键分析
    • 通过反集(敌人域)实现敌人的敌人合并为朋友,例如 a 的敌人域与 b 的朋友域合并。
  • 代码片段
    if (op == 'E') {
        merge(a, b + n); // a的敌人域与b的朋友域合并
        merge(b, a + n);
    }
例题4. ​P1536 村村通 
  • 问题描述:计算使所有村庄连通最少需要新增的道路数。
  • 实现思路
    • 合并所有已有道路的村庄。
    • 统计最终独立集合的数量 k,答案即 k−1。
  • 剪枝技巧
    • 路径压缩:优化查询效率。
    • 连通块统计:遍历所有节点,统计根节点数量。

3、带权并查集

例题5. ​P1196 [NOI2002] 银河英雄传说 
  • 问题描述:计算战舰队列中两艘战舰之间的间隔,支持合并队列与查询距离。
  • 实现思路
    • 带权并查集:维护每个节点到根节点的距离 d[x]。
    • 合并时更新距离权值,路径压缩时同步更新权值。
  • 关键代码
    int find(int x) {
        if (fa[x] == x) return x;
        int root = find(fa[x]);
        d[x] += d[fa[x]]; // 更新距离权值
        return fa[x] = root;
    }

4、总结与优化策略

  1. 基础操作:路径压缩和按秩合并是并查集的核心优化,能将时间复杂度降至接近 O(1)。
  2. 扩展应用
    • 扩展域:处理多关系问题(如敌友关系)。
    • 带权并查集:维护节点间的附加信息(如距离、排名)。
  3. 适用场景
    • 连通性判断(亲戚、网络连接)。
    • 动态合并与查询(图论、集合关系维护)。

五. 贪心算法(Greedy)

难点:正确性证明和贪心策略的选择
典型问题:区间调度(选择最多不重叠区间)
代码示例

#include <vector>
#include <algorithm>
using namespace std;

struct Interval {
    int start, end;
};

bool compare(Interval &a, Interval &b) {
    return a.end < b.end; // 按结束时间排序
}

int maxIntervals(vector<Interval> intervals) {
    sort(intervals.begin(), intervals.end(), compare);
    int count = 0, last_end = -1;
    for (auto &itv : intervals) {
        if (itv.start >= last_end) {
            count++;
            last_end = itv.end;
        }
    }
    return count;
}

关键点

  • 贪心策略:优先选择结束时间早的区间。

  • 正确性证明:局部最优(选最早结束)导致全局最优。

1、基础贪心问题

例题1. ​P1090 [NOIP 2004 提高组] 合并果子
  • 问题描述:将多堆果子合并成一堆,每次合并消耗体力等于两堆重量之和,求最小总消耗。
  • 贪心策略:每次选择当前最小的两堆合并,重复直到只剩一堆。使用优先队列(小根堆)优化选择过程。
  • 实现代码
    priority_queue<int, vector<int>, greater<int>> pq;
    // 插入所有果子重量到优先队列
    while (pq.size() > 1) {
        int a = pq.top(); pq.pop();
        int b = pq.top(); pq.pop();
        sum += a + b;
        pq.push(a + b);
    }
  • 关键点:优先队列保证每次取最小两堆,时间复杂度 O(n logn)。

例题2. ​P4995 跳跳!​
  • 问题描述:从地面跳到不同高度的平台,每次跳跃消耗体力为高度差的平方,求最大总消耗。
  • 贪心策略:排序后交替选择最大和最小的平台,形成“最大差值跳跃链”。
  • 代码片段
    sort(s.begin(), s.end());
    while (!s.empty()) {
        last = s.back(); s.pop_back();
        sum += (last - first) * (last - first);
        if (!s.empty()) {
            first = s.front(); s.pop_front();
            sum += (last - first) * (last - first);
        }
    }
  • 分析:通过排序和双端操作最大化每次跳跃的差值,时间复杂度 O(nlogn)。

2、经典贪心模型

例题3. ​P1223 排队接水
  • 问题描述:安排 n 个人接水顺序,使平均等待时间最小。
  • 贪心策略:按接水时间升序排序,短任务优先处理。
  • 代码核心
    sort(t, t + n); // 按接水时间排序
    double sum = 0;
    for (int i = 0; i < n; i++) {
        sum += t[i] * (n - i - 1); // 后续所有人的等待时间累加
    }
  • 时间复杂度:O(nlogn),正确性证明可通过反证法。

例题4. ​P1080 国王游戏
  • 问题描述:安排大臣左右手的金币数排列,使得获得最多奖赏的大臣的奖赏尽可能少。
  • 贪心策略:按左右手乘积升序排列,乘积小的排前面。
  • 代码思路
    bool cmp(Node a, Node b) {
        return a.left * a.right < b.left * b.right;
    }
    // 排序后遍历计算最大值
  • 难点:需结合高精度计算处理大数相乘,贪心正确性需数学归纳法证明。

3、进阶贪心应用

例题5. ​P1094 纪念品分组
  • 问题描述:将纪念品分组,每组最多两件且总价值不超过 w,求最少组数。
  • 贪心策略:排序后使用双指针,最大与最小配对。
  • 实现代码
    sort(p, p + n);
    int i = 0, j = n - 1, ans = 0;
    while (i <= j) {
        if (p[i] + p[j] <= w) i++, j--;
        else j--;
        ans++;
    }
  • 优化点:通过双指针将时间复杂度降至 O(n)。

例题6. ​P1230 智力大冲浪
  • 问题描述:在截止时间内完成游戏任务,避免扣款最大化剩余奖金。
  • 贪心策略:按扣款降序排序,优先处理扣款高的任务,尽量在截止时间前安排。
  • 实现方法:使用标记数组记录时间段是否被占用,按扣款顺序尝试填充时间槽。

4、总结与刷题建议

  1. 贪心核心:通过局部最优选择构造全局最优解,常见策略包括排序、优先队列、双指针等。
  2. 洛谷推荐题单
    • 基础:P1421(模拟)、P1048(背包贪心)
    • 进阶:P2671(区间贪心)、P1880(动态规划与贪心结合)
  3. 注意点:贪心算法需严格证明正确性,例如反证法或数学归纳法。

全部总结

  1. 动态规划:从简单模型(如背包)入手,理解状态定义。

  2. 图论:熟练Dijkstra和Floyd算法的应用场景。

  3. 搜索:合理剪枝,避免暴力超时。

  4. 并查集:必须掌握路径压缩优化。

  5. 贪心:多练习经典问题,如区间问题、哈夫曼编码等。

还有争取AK csp-J

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值