算法算法~

动态规划

基本步骤

  1. 穷举分析
  2. 确定边界
  3. 找出规律,确定最优子结构
  4. 写出状态转移方程

Tips

  1. 为了不爆表, 数组都开到全局变量里

背包

只考虑第 i 件物品放或不放,转化为一个只牵扯前 i - 1 件物品的问题。
不放第 i 件物品,转化为“前 i - 1 件物品放入容量为 v 的背包中”
果放第 i 件物品,转化为“前 i - 1 件物品放入剩下的容量为 v - c[i] 的背包中”,此时能获得的最大价值就是 dp[i -1]v - c[i]再加上通过放入第 i 件物品获得的价值 w[i]

从序列中选择子序列使得和接近target的题目,一般都是双向dfs或者01背包问题来完成。

状态转移方程

dp[i][j] = fmax(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
/*
用法:
    对每个物品调用对应的函数即可, 例如多重背包:
    for(int i = 0; i < N; i++)
        multiple_pack_step(dp, w[i], v[i], num[i], W);

参数:
    dp   : 空间优化后的一维dp数组, 即dp[i]表示最大承重为i的书包的结果
    w    : 这个物品的重量
    v    : 这个物品的价值
    n    : 这个物品的个数
    max_w: 书包的最大承重
*/
void zero_one_pack_step(vector<int> &dp, int w, int v, int max_w)
{
    for (int j = maxWeight; j >= weight; j--) // 反向枚举!!!
        dp[j] = fmax(dp[j], dp[j - weight] + value);
}

void complete_pack_step(vector<int> &dp, int w, int v, int max_w)
{
    for (int j = w; j <= max_w; j++) // 正向枚举!!!
        dp[j] = fmax(dp[j], dp[j - w] + v);

    // 法二: 转换成01背包, 二进制优化
    // int n = max_w / w, k = 1;
    // while(n > 0){
    //     zero_one_pack_step(dp, w*k, v*k, max_w);
    //     n -= k;
    //     k = k*2 > n ? n : k*2;
    // }
}

void multiple_pack_step(vector<int> &dp, int w, int v, int n, int max_w)
{
    if (n >= max_w / w)
        complete_pack_step(dp, w, v, max_w);
    else
    { // 转换成01背包, 二进制优化
        int k = 1;
        while (n > 0)
        {
            zero_one_pack_step(dp, w * k, v * k, max_w);
            n -= k;
            k = k * 2 > n ? n : k * 2;
        }
    }
}

0-1 背包

二维

dp[i][j] 前 i 个物品,背包容量 j 下的最优解

  1. dp[0][0] = 0开始决策
  2. 当前背包容量不够,只能从前面开始
    dp[i][j] = dp[i - 1][j];
    
  3. 容量够,从选或不选后较大值确定
dp[i][j] = fmax(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
vector<int> w(100001), v(100001), dp(100001, 0);
int main()
{
    int n, i, j, c;
    cin >> n >> c;
    for (i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    for (i = 1; i <= n; i++)
    {
        for (j = 1; j <= c; j++)
        {
            if (j < v[i])
                dp[i][j] = dp[i - 1][j];
            else
                dp[i][j] = fmax(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][c];
} 
一维

为什么可以变形为一维呢?题目只要求最大值,只记录结果就好

为什么一维情况下枚举背包容量需要逆序?

在二维情况下,状态 dp[i][j]是由上一轮 i - 1 的状态得来的,f[i][j]与 dp[i - 1][j]是独立的。

而优化到一维后,如果我们还是正序,则有 dp[较小体积]更新到 dp[较大体积],则有可能本应该用第 i-1 轮的状态却用的是第 i 轮的状态。

一维情况正序更新状态 dp[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

dp[j] = fmax(dp[j], dp[j - v[i]] + w[i]);

二维下的状态定义 dp[i][j]是前 i 件物品,背包容量 j 下的最大价值。

一维下,少了前 i 件物品这个维度,我们的代码中决策到第 i 件物品(循环到第 i 轮),f[j]就是前 i 轮已经决策的物品且背包容量 j 下的最大价值。

因此当执行完循环结构后,由于已经决策了所有物品,f[j]就是所有物品背包容量 j 下的最大价值。

一维 dp[j]等价于二维 dp[n][j]。

vector<int> w(100001), v(100001), dp(100001, 0);
int main()
{
    int n, i, j, c;
    cin >> n >> c;
    for (i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    for (i = 1; i <= n; i++)
    {
    	for(j = c; j >= v[i]; j--)
            dp[j] = fmax(dp[j], dp[j - v[i]] +w[i]);
    }
    cout << dp[c];
}

优化后的, 一遍输入 一边处理

int dp[100001];
int main()
{
    int n, m;
    cin >> n >> m;

    for(int i = 1; i <= n; i++) 
    {
        int v, w;
        cin >> v >> w;      // 边输入边处理
        for(int j = m; j >= v; j--)
            dp[j] = fmax(dp[j], dp[j - v] + w);
    }

    cout << dp[m] << endl;
}

分割等和子集

一个只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

思路 – 不超过sum/2的前提下,所能形成的最大数组序列和是多少

作为「0-1 背包问题」,它的特点是:每个数只能用一次

物品一个一个选,容量也一点一点增加去考虑

从整个好数组中挑出一些正整数,使得这些数的和 == 整个数组的元素的一半

将每个元素的值看作是物品的重量,每件物品的价值都为1 就是一个恰好装满的01背包问题

数组的和一定得是偶数。

dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。

不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;

选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。

dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]

∴将dp[0] = 0; 其他全部初始化为INF_MIN

int capacity = sum / 2;
vector<int>dp(capacity + 1, INT_MIN);
dp[0] = 0;
for(int i = 1; i <= n; i++)
    for(int j = capacity; j >= nums[i-1]; j--)
        dp[j] = fmax(dp[j], 1 + dp[j - nums[i-1]]);

如果dp[sum / 2] > 0 return true

Code
bool canPartition(vector<int> &nums)
{
    int sum = 0, n = nums.size();
    for (int &num : nums)
        sum += num;
    if (sum % 2 != 0)
        return false;

    int capacity = sum / 2;
    vector<bool> dp(capacity + 1, false);
    dp[0] = true;
    for (int i = 1; i <= n; i++)
        for (int j = capacity; j >= nums[i - 1]; j--)
            dp[j] = dp[j] || dp[j - nums[i - 1]];

    return dp[capacity];
}

因为只是求能不能划分, 最后改为bool类型

另一种思路

采用一个bitset来记录所有可能的和。具体步骤是: 开辟一个大小为5001的bisets(因为所有元素和不超过10000)名为bits,最后得到的bits满足bits[i]=1则代表nums中某些元素的和为i,最后判断bits[sum/2]是否为1即可。处理方法为:

初始时bits[0] = 1,然后从前往后遍历nums数组,对于当前遍历到的数字num,把 bits 向左平移 num 位,然后再或上原来的 bits,这样就代表在原先的基础上又新增了一个和的可能性。 比如对于数组 [1,3],初始化 bits 为 …00001,遍历到1,bits 变为…00011,然后遍历到3,bits 变为了 …11011。最终得到的bit在第1,3,4位上为1,代表了可能的和为1,3,4,这样遍历完整个数组后,去看 bits[sum/2] 是否为1即可。

class Solution
{
public:
    bool canPartition(vector<int> &nums)
    {
        bitset<5001> bits(1);
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum & 1)
            return false; // sum为奇数
        for (int &num : nums)
            bits |= (bits << num);
        return bits[sum >> 1];
    }
};

最后一块石头的重量

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

思路
  1. 确定dp数组下标含义: dp[j] 表示: 容量为j的背包,所背的石头最大重量为dp[j]
  2. 递推公式 : dp[j] = fmax(dp[j], dp[ j - stones[i]]+stones[i])
  3. 初始化 : 最大重量就是30 * 1000 ,target 为 15000
  4. 遍历顺序 外层物品,内层背包
  5. 推导结果 (sum - dp[target]) - dp[target]
Code

int lastStoneWeightII(int[] stones)
{
    int sum = 0;
    for (int num : stones)
    {
        sum += num;
    }
    int target = sum / 2;
    int dp[] = new int[target + 1];
    for (int i = 0; i < stones.length; i++)
    {
        for (int j = target; j >= stones[i]; j--)
        {
            dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
        }
    }
    return (sum - dp[target]) - dp[target];
}

目标和

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

dp[i][j]定义为从数组nums中 0 - i 的元素进行加减可以得到 j 的方法数量
int findTargetSumWays(vector<int> &nums, int target)
{
    int sum = 0;
    for (int &num : nums)
    {
        sum += num;
    }
    int diff = sum - target;
    if (diff < 0 || diff % 2 != 0)
    {
        return 0;
    }
    int neg = diff / 2;
    vector<int> dp(neg + 1);
    dp[0] = 1;
    for (int &num : nums)
    {
        for (int j = neg; j >= num; j--)
        {
            dp[j] += dp[j - num];
        }
    }
    return dp[neg];
}

完全背包

题目

有 N 种物品和一个容量为 C 的背包,每种物品都有无限件可用。第 i 种物品的费用是 c[i] ,价值是 w[i]

求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路
  1. 01 背包一个物品只能选 1 次

    完全背包同个物品可以选多次。

  2. 01 背包优化版的 j 从 m->v[i] 会只使用一次 v[i]

    完全背包优化版的 j 从 v[i] -> m 可以反复使用 v[i]( dp[m] 可能会使用到 dp[v[i] 的值,这就相当于 dp[m] 又装入了一次 i 物品)

  3. 01 背包为自上而下(逆序遍历)
    完全背包为自底向上(正序遍历)。

  4. 01 背包可以求最大值,也可以求最小值。

只需把 01 背包问题的代码中的 j 改为从小到大遍历即可。

二维
int w[N], v[N];
int dp[N][N];

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> w[i] >> v[i];

    for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
    {
        int t = j / w[i];
        for (int k = 0; k <= t; k++)
            dp[i][j] = fmax(dp[i][j], dp[i - 1][j - k * w[i]] + k * v[i]);
    }
    cout << dp[n][m];
}
一维
int dp[100001];
int main()
{
    int N, V;
    cin >> N >> V;

    for (int i = 1; i <= N; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = w; j <= V; j++)  // 只需修改这一行
            dp[j] = fmax(dp[j], dp[j - w] + v);
    }

    cout << dp[V] << "\n";
}

多重背包

现有 N 种物品和一个最多能承重 M 的背包,第 i 种物品的数量是 si,重量是 w,价值是 v。在背包能承受的范围内,试问将哪些物品装入背包后可使总价值最大,求这个最大价值。

用二维 code更短

直接用模版套一位,会爆掉
回顾完全背包问题的暴力解法,在背包承重为 j 的前提下,第种物品最多能放 t = /w 个(这里是整除)。而在 01 背包问题中,第 i 种物品只有一个,所以应当取 t = min(1,j/w[i])。由此可见,对于多重背包问题,只需取 t = min(s,j/w[i])。

二维
int w[N], v[N];
int dp[N][N];

int main()
{
    int n, m;
    cin >> n >> m;
    int weight, value, num;  //重量  价值  数量
    for (int i = 1; i <= n; i++)
    {
        cin >> weight >> value >> num; 
        for (int j = 1; j <= m; j++) 
        {
            int t = fmin(num, value / weight);  // 只有这里需要修改
            for (int k = 0; k <= t; k++)
                dp[i][j] = fmax(dp[i][j], dp[i - 1][j - k * w] + k * v);
        }
    }
    cout << dp[n][m];
}
一维
#include <bits/stdc++.h>
using namespace std;
const int N = 11010, M = 2010;

int w[N], v[N];
int dp[M];

int main() {
    int n, m;
    cin >> n >> m;
    int weight, value, num;  // w是重量, v是价值, c是数量

    int cnt = 0;
    while (n--) {
        cin >> weight >> value >> num;
        for (int k = 1; k <= num; k *= 2) {
            cnt++;
            w[cnt] = a * k, v[cnt] = b * k;
            num -= k;
        }
        if (s > 0) {
            cnt++;
            w[cnt] = a * s, v[cnt] = b * num;
        }
    }

    n = cnt;
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= w[i]; j--)
            dp[j] = fmax(dp[j], dp[j - w[i]] + v[i]);

    cout << dp[m] << "\n";
}

分组背包

题目

有 N 组物品和一个容量是 V 的背包
每组物品有若干个,同一组内的物品最多只能选一个每件物品的体积是 i,价值是 wi,其中 是组号,j是组内编号
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 110;

int w[N][N], v[N][N], s[N];
int dp[N];

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> s[i];
        for (int j = 1; j <= s[i]; j++)
            cin >> w[i][j] >> v[i][j];
    }

    for (int i = 1; i <= n; i++)
        for (int j = m; j >= 1; j--)
            for (int k = 1; k <= s[i]; k++)
                if (j >= w[i][k])
                    dp[j] = fmax(dp[j], dp[j - w[i][k]] + v[i][k]);

    cout << dp[m] << "\n";
}

2022 的合数

题目描述

将 2022 拆分成 10 个互不相同的正整数之和,总共有多少种拆分方法?
注意交换顺序视为同一种方法,例如 2022 =1000 -1022 和 20221022 + 1000 就视为同一种方法。

0-1 背包拓展

有 2022 个物品,它们的编号分别是 1 到 2022,它们的体积分别等于它们的编号。

从 2022 个物品种选取 10 个物品,满足 10 个物品的体积之和为 2022 用 dp[i][j][k]表示前 i 个物品里选择 j 个物品,体积之和为 k 的方案数 则对于前 i 种物品,有两种选择,选或者不选 dp[i][j][k]=dp[i-1][j][k] 不选 dp[i][j][k]=dp[i-1][j-1][k-i] 选 (为什么是 k-i,因为第 i 个物品的体积就是 i)

  1. 定义 dp[][][], dp[i][j][k]表示从数字 1~i 取 j 个和为 k 的方案数
    (1) k >= i 时, 数 i 可以要,也可以不要.
    ① 要 => 从 1 到 i - 1 中 j - 1 个数

[区间DP – 求一段小区间上的最优解]

要求某个区间上的最优解,那就分成一个一个的小区间,求解每个小区间的最优解,再合并得到大区间

枚举 小区间长度为len, 内层枚举len下可以的起点, 终究就是 i + len然后在小区间下枚举分割点,求解这段小区间在某个分割点下的最优解

for (int len = 1; len <= n; len++) // 枚举长度
{ 
    for (i = 1; i + len <= n + 1; i++) // 枚举起点,ends<=n
    { 
        int end = i + len - 1;
        for (j = i; j < end; j++) // 枚举分割点,更新小区间最优解
        { 
            dp[i][end] = min(dp[i][end], dp[i][j] + dp[j + 1][end] + something);
        }
    }
}

状态dp[i][j] 将下标位置从i到j的所有元素合并能获得的价值的最大值

dp[i][j] = min(dp[i][k - 1] + dp(k, j)) +w[i][j]; (i < k <= j)
//w表示在转移时需要付出的额外代价
//min 也可以是 fmax

石子合并 - 链

由n堆石子排成一排,其编号为1,2,3……,n。每堆石子有一定的质量mi(mi<=1000),现在要将这n堆石子合并成一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,由于合并顺序的不同,导致合并成一堆石子的总代价也不同,请求出最少的代价将所有石子合并为一堆。

dp + 前缀和

i~j合并 = 更小的 (原来, 分割点i左边的重量+ 分割点i右边的重量 + 合并后两队的重量)

dp[i][j] = fmin(dp[i][j], dp[i][j] + dp[i + 1][j] + sum[j] - sum[i- 1]);
#include <bits/stdc++.h>

using namespace std;
const int MAX = 1005;
const int INF = 0x3f3f3f3f;
int dp[MAX][MAX];
int sum[MAX];
int a[MAX];
int i, j, n, len;
int main()
{
    cin >> n;
    memset(sum, 0, sizeof sum);
    memset(dp, INF, sizeof dp);
    for (i = 1; i <= n; i++)
    {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
        dp[i][i] = 0;
    }

    for (len = 1; len <= n; len++)
    {
        for (i = 1; i + len <= n + 1; i++)
        {
            int end = i + len - 1;
            for (j = i; j < end; j++)
            {
                dp[i][end] = fmin(dp[i][end], dp[i][j] + dp[j + 1][end] + sum[end] - sum[i - 1]);
            }
        }
    }
    cout << dp[1][n] << endl;
    return 0;
}

石子合并 - 环

在一个圆形操场的四周摆放 N N N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 2 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 N N N 堆石子合并成 1 1 1 堆的最小得分和最大得分。

将链延长为2倍, 变成2 * N 堆

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
第i堆与第i + n 堆相同

dp后 , 取dp[1][n], dp[2][n + 1]…dp[n - 1][2 * n - 2]中的最优值

for (len = 2; len <= n; len++)
{
    for (i = 1; i <= 2 * n - 1 - len; i++)
    {
        int j = len + i - 1;
        for (k = i; k < j; k++)
            dp[i][j] = fmax(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
    }
}
  1. 我们为了将环形变为直线,必须规定转动顺序,这里采用逆时针转动,且以 i 作为起点,j作为终点。(右下图)

  2. 当规定好终点了,那么这环形有4种情况,我们求在这四种情况下最下的。(右上图)

  3. 关于转换成直线,比如存在 a(0) -> b(1) -> c(2) -> d(3) 与 d(3) -> a(4) -> b(5) -> c(6)。

第一条是 i=0,j=3的数组,第二条是 i=3,j=6 的数组。 这样,我们就不用返回去计算了( 3->0->1->2 ???)。

  1. 四种情况分别是数组中的四行,每行最后一个代表遍历的结果,我们最后只需要遍历这四个值,找到最小值即可。

Code

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;

int a[300], sum[300];
int Min[300][300], Max[300][300];
int main()
{
    int n, i;
    cin >> n;

    for (i = 1; i <= n; i++)
    {
        cin >> a[i];
        a[i + n] = a[i];
    }

    // 计算最大和
    for (i = 1; i <= 2 * n; i++)
    {
        sum[i] = sum[i - 1] + a[i];
    }

    for (i = 2 * n - 1; i >= 1; i--)
    {
        for (int j = i + 1; j < i + n; j++)
        {
            Min[i][j] = INF;
            for (int k = i; k < j; k++)
            {
                Min[i][j] = min(Min[i][j], Min[i][k] + Min[k + 1][j] + sum[j] - sum[i - 1]);
                Max[i][j] = fmax(Max[i][j], Max[i][k] + Max[k + 1][j] + sum[j] - sum[i - 1]);
            }
        }
    }

    int MaxValue = 0, MinValue = INF;
    for (i = 1; i <= n; i++)
    {
        MaxValue = fmax(MaxValue, Max[i][i + n - 1]);
        MinValue = fmin(MinValue, Min[i][i + n - 1]);
    }

    cout << MinValue << endl
         << MaxValue << endl;
}

树形 DP – 树上进行的 DP,递归进行

没有上司的舞会

某大学有 n n n 个职员,编号为 1 … n 1\ldots n 1n

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r i r_i ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

思路

dp[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值

dp[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值

则f[x][0]=sigma{fmax(dp[y][0],dp[y][1])} (y是x的儿子)

dp[x][1]=sigma{dp[y][0]}+h[x] (y是x的儿子)

先找到唯一的树根root

则ans=fmax(dp[root][0],dp[root][1])

code

#include <bits/stdc++.h>
#define N 6001
using namespace std;
int ind[N], n, hap[N], dp[N][2], fa[N], root, vis[N], ne[N], po[N];
void work(int x)
{
    for (int i = po[x]; i; i = ne[i])
    {
        work(i);
        dp[x][1] = fmax(fmax(dp[x][1], dp[x][1] + dp[i][0]), dp[i][0]);
        dp[x][0] = fmax(fmax(dp[x][0], dp[i][1] + dp[x][0]), fmax(dp[i][1], dp[i][0]));
    }
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> dp[i][1];
    for (int i = 1; i < n; i++)
    {
        int a, b;
        cin >> b >> a;
        ind[b]++;
        ne[b] = po[a];
        po[a] = b;
    }
    for (int i = 1; i <= n; i++)
        if (!ind[i])
        {
            root = i;
            break;
        }
    work(root);
    cout << fmax(dp[root][0], dp[root][1]) << endl;
}

选课

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N N N 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 M M M 门课程学习,问他能获得的最大学分是多少?

思路

每门课最多只有一门选修课 => 与有根树中一个点最多只有一个父节点的特点相似

新增一门0学分的课程(编号为0),作为所有无先修课程的先修课,这样就变成了以0好课程为根的树

这样的好处:

  1. 一棵树就不用分别考虑各科树然后再合并
  2. 输入方便,不用处理0
Code
#include <bits/stdc++.h>
using namespace std;
int dp[305][305], s[305], n, m;
vector<int> e[305];

int dfs(int u)
{
    int p = 1;
    dp[u][1] = s[u];
    for (auto v : e[u])
    {
        int siz = dfs(v);
        // 注意下面两重循环的上界和下界
        // 只考虑已经合并过的子树,以及选的课程数超过 m+1 的状态没有意义
        for (int i = min(p, m + 1); i; i--)
            for (int j = 1; j <= siz && i + j <= m + 1; j++)
                dp[u][i + j] = fmax(dp[u][i + j], dp[u][i] + dp[v][j]); // 转移方程
        p += siz;
    }
    return p;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        int k;
        cin >> k >> s[i];
        e[k].push_back(i);
    }
    dfs(0);
    printf("%d", dp[0][m + 1]);
}

状态压缩 – 化繁为简

需要考虑的状态非常多,我们如果用平常的思想去表示状态,那是非常不现实的,在时间和空间上都不允许,我们使用某种方法,以最小的代价表示某种状态。

适用场景

  1. 状态需要有一定的状态单元,一个状态应该是保存一个集合,其中的元素值对应着0 或1
  2. 题目中限制的集合大小不会超过20。
int n;
int maxn = 1 << n; // 总状态数。
// 枚举已有的集合数。按照状态转移的顺序,一般从小编号到大编号。
for (i = 1; i <= m; i++)
{
    // 枚举当前集合中的状态。
    for (j = 0; j < maxn; j++)
    {
        // 判断当前集合是否处于合法状态,通常我们需用一个数组提前处理好。如g数组;
        if (当前状态是否合格)
        {
            for (k = 0; k < maxn; ++k)
            {
                // 枚举上一个集合的状态。
                if (上一个集合的状态是否合格 + 上一个集合的状态和当前状态的集合是否产生了冲突)
                {
                    列写状态转移方程。
                }
            }
        }
    }
}

Corn Fields G

题目描述

农场主 J o h n \rm John John 新买了一块长方形的新牧场,这块牧场被划分成 M M M N N N ( 1 ≤ M ≤ 12 , 1 ≤ N ≤ 12 ) (1 \le M \le 12, 1 \le N \le 12) (1M12,1N12),每一格都是一块正方形的土地。 J o h n \rm John John 打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是 J o h n \rm John John 不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

J o h n \rm John John 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

思路

对于每一行,我们就可以看成一个未知集合,而集合的大小自然就是列m

对于每一个单元,其取值范围为0, 1 ,而1 代表放置奶牛,0 代表不放置奶牛

用二进制表示,那么状态总数就是( 1 << m ) − 1
对于每一个状态,我们需要判断是否合格,而其中明确不能选择两块相邻的土地,在集合内,即相邻位不能全为1 ,所以我们可以预处理g 数组,处理方式即为:g[i] = !(i & (i << 1))

同样,我们还应该知晓土地的状况,因为毕竟只有土地肥沃才可以放置奶牛,则我们可以通过一个st数组判断,集合与集合之间,我们也需要考虑相邻位不能全为1,所以在枚举上一个集合的状态也需要严格判断。对于状态定义,我们可以用f[i][j]表示第i 行且状态为j jj的方案数。对于状态转移,假设上一行状态为k kk,则状态转移方程为:

f[i][j] += f[i - 1][k];
Code
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int N = 10 + 5, M = 10 + 5;
const int P = 1e8;

int n, m, i, j;     // n行m列的土地。
int a[N][M], st[N]; // a代表土地,st代表每一行的土地状况。
bool g[1 << N];     // g得到所有状态中的合法状态。
int f[N][1 << N];   // f[i][j]表示的则是第i行且状态为j的方案数,是由上一行转移过来的,所以我们定义上一行的状态为k。
// 则状态转移方程为f[i][j] += f[i - 1][k];//其中j和k必须满足条件。
int main()
{
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        for (j = 1; j <= m; j++)
        {
            scanf("%d", &a[i][j]);
        }
    }
    // 得到每一行的土地状况。
    for (i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            st[i] = (st[i] << 1) + a[i][j];
        }
    }
    // 得到所有状态中的合法状态。
    int maxn = 1 << m; // 总状态。
    f[0][0] = 1;       // 初始化,这种也算一种。
    
    for (i = 0; i < maxn; i++)
    {
        g[i] = !(i & (i << 1)); // 由于不能相邻,所以我们左移判断是否符合条件。
    }

    for (i = 1; i <= n; i++)
    {
        // 枚举每一行。
        for (int j = 0; j < maxn; j++)
        {
            // 枚举每一行的状态,判断此状态是否符合条件。1.不能相邻。2.是全部状态的子集。
            if (g[j] && (j & st[i]) == j)
            {
                // 如果符合条件。则我们去判断上一行是否符合。
                for (int k = 0; k < maxn; ++k)
                {
                    // 枚举上一行状态。注意,这里我们无需判断上一行状态是否存在,因为不存在即为0.
                    // 只需要判断j和k是否存在相邻草地。
                    if (!(j & k))
                    {
                        f[i][j] = (f[i][j] + f[i - 1][k]) % P;
                    }
                }
            }
        }
    }
    int ans = 0;
    for (int j = 0; j < maxn; j++)
    {
        ans = (ans + f[n][j]) % P;
    }
    cout << ans << endl;
}

单调队列

定义

队列内的元素有单调性

所以经常被用来维护区间的最值和降低dp维数

求m区间内的最小值
题目描述

一个含有 n n n 项的数列,求出每一项前的 m m m 个数到它这个区间内的最小值。若前面的数不足 m m m 项则从第 1 1 1 个数开始,若前面没有数则输出 0 0 0

思路

每一个答案只与当前下标的前m个有关,所以可以用单调队列维护前m个的最小值

using namespace std;
#define maxn 2000009
int n, m, k, tot, head, tail, a[maxn], q[maxn];
int main()
{   
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    head = 1, tail = 0; // 起始位置为1 图为超入是q[++tail]所以要初始化为8

    for (int i = l; i <= n; i++) // 每次队首的元素就是当前的答案
       cout << a[q[head]] << endl;

    while (i - q[head] + 1 > m && head <= tail) // 维护风
        head++;
    while (a[i] < a[q[tail]] & 8head <= tail) // 维产风尾
        tail--;
    q[++tail] = i; // 存入下标
}

数位 DP

在数的每一位上进行dp

ll dfs(int pos, int pre, int st,……, int lead, int limit) // 记搜
{
    if (pos > len)
        return st; // 剪枝
    if ((dp[pos][pre][st]……[……] != -1 && (!limit) && (!lead)))
        return dp[pos][pre][st]……[……];      // 记录当前值
    ll ret = 0;                             // 暂时记录当前方案数
    int res = limit ? a[len - pos + 1] : 9; // res当前位能取到的最大值
    for (int i = 0; i <= res; i++)
    {
        // 有前导0并且当前位也是前导0
        if ((!i) && lead)
            ret += dfs(……,……,……, i == res && limit);
        // 有前导0但当前位不是前导0,当前位就是最高位
        else if (i && lead)
            ret += dfs(……,……,……, i == res && limit);
        else if (根据题意而定的判断)
            ret += dfs(……,……,……, i == res && limit);
    }
    if (!limit && !lead)
        dp[pos][pre][st]……[……] = ret; // 当前状态方案数记录
    return ret;
}
ll part(ll x) // 把数按位拆分
{
    len = 0;
    while (x)
        a[++len] = x % 10, x /= 10;
    memset(dp, -1, sizeof dp); // 初始化-1(因为有可能某些情况下的方案数是0)
    return dfs(……,……,……,……);   // 进入记搜
}

int main()
{
    cin >> T;
    while (T--)
    {
        cin >> l >> r;
        if (l)
            printf("%lld", part(r) - part(l - 1)); //[l,r](l!=0)
        else
            printf("%lld", part(r) - part(l)); // 从0开始要特判
    }
    return 0;
}

例题

股票买卖

正则表达式匹配

一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

递归

分是否匹配a这个东西分两种情况进行讨论

首先判断第一个字符是否匹配,然后判断是否是模式,是的话可以匹配这个字符也可以不匹配,如果不是模式的话就判断后面。

bool isMatch(string s, string p)
{
    int plen = p.size(), slen = s.size();

    if (plen == 0)
        return slen == 0;

    // 是否匹配第一个
    bool matchfirst = (slen != 0 && (s[0] == p[0] || p[0] == '.'));
    
    // 是否是*模式
    if (plen >= 2 && p[1] == '*')
        return (isMatch(s, p.substr(2)) || (matchfirst && isMatch(s.substr(1), p)));
    else
        return (matchfirst && isMatch(s.substr(1), p.substr(1)));
}
dp

dp[i][j]表示isMatch(s.strsub(i), p.strsub(j))

dp[i][j+2], *模式时不匹配的情况
dp[i+1][j], 模式,并且第一个字符匹配成功的情况
dp[i+1][j], 非
模式,第一个字符匹配成功的情况

bool isMatch(string s, string p)
{
    int plen = p.length(), slen = s.length();

    vector<vector<bool>> dp(slen + 1, vector<bool>(plen + 1, false));
    dp[slen][plen] = true;

    for (int i = slen; i >= 0; i--)
    {
        for (int j = plen - 1; j >= 0; j--)
        {
            // 判断第一个是否匹配
            bool match = (i < slen && (s[i] == p[j] || p[j] == '.'));

            // 是*模式,两种方案,匹配/不匹配
            if (j + 1 < plen && p[j + 1] == '*')
                dp[i][j] = dp[i][j + 2] || (match && dp[i + 1][j]);
            // 非*
            else
                dp[i][j] = (match && dp[i + 1][j + 1]);
        }
    }
    return dp[0][0];
}

k-e-Tree

题目

最近有一个富有创造力的学生Lesha听了一个关于树的讲座。在听完讲座之后,Lesha受到了启发,并且他有一个关于k-tree(k叉树)的想法。 k-tree都是无根树,并且满足:

每一个非叶子节点都有k个孩子节点;
每一条边都有一个边权;
每一个非叶子节点指向其k个孩子节点的k条边的权值分别为1,2,3,…,k。
当Lesha的好朋友Dima看到这种树时,Dima马上想到了一个问题:“有多少条从k-tree的根节点出发的路上的边权之和等于n,并且经过的这些边中至少有一条边的边权大于等于d呢?” 现在你需要帮助Dima解决这个问题。考虑到路径总数可能会非常大,所以只需输出路径总数 mod 1000000007 即可。(1000000007=10^9+7)

思路

求出多少条路径,从跟节点出发,满足路径上至少有一条边的权重≥ d, 且路径上的边权综合为n

题目看上去是对树的考察,实际不是, 但是可以从树的结构分析

最小权重值 必须要维护/记录这个值, 作为一个单独的变量来思考
保留最小,存在<d 无法确定, 所以应该保留最大
定义数组dp[L][R]. L到达某个结点时, 经过所有边的权重和,R表示经过路径中权重最大的边的大小

从当前节点出发, 依次向权重为1、2、3…k的边走

dp[L + k][max(R, k)] = dp[L + k][max(R, k)] + dp[L][R];
  1. k 与 n的大小不确定, 要考虑k > n的情况
  2. 要考虑 当前节点能否在之前节点的前提下往下走
#include <iostream>
#define int long long
using namespace std;

const int mod = 1e9 + 7;
const int N = 110;

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = -1;
    for (; isdigit(ch); ch = getchar())
        x = (x << 3) + (x << 1) + ch - '0';
    return x * f;
}
int f[N][2];
signed main()
{
    int n = read(), k = read(), d = read();
    f[0][0] = 1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= min(i, k); j++)
        {
            if (j < d)
                f[i][0] += f[i - j][0], f[i][1] += f[i - j][1];
            else
                f[i][1] += f[i - j][1] + f[i - j][0];
            f[i][1] %= mod, f[i][0] %= mod;
        }
    }
    printf("%lld\n", f[n][1]);
    return 0;
}

[分治法]

三者区别

1.分治:将问题域划分为多个子问题域,然后都这些问题域分别求解后,在将所得的所有解融合。

2.贪心:将问题域划分为一个子问题域,然后都这些问题域分别求解后,在将所得的所有解融合。

3.动态规划:将计算过程中的结果保存下来重复使用,避免无必要的重复计算。

通用步骤:

step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题

step3 合并:将各个子问题的解合并为原问题的解。

递归模版

 Divide-and-Conquer(P)

 1. if |P|≤n0

 2. return(ADHOC(P))

 3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk

 4. for i←1 to k

 5. do yi ← Divide-and-Conquer(Pi) // 递归解决Pi

 6. T ← MERGE(y1,y2,...,yk) // 合并子问题

 7. return(T)
res dc(problem p)
{
  if (|p| < N)
    solve(p); // 如果问题规模小到可以求解,那么求解子问题

  for(i = 0; i < k;i++) // 否则分解问题
    y = dc(p[i]); // 递归求解各个子问题

  return merge(y);//合并各个元子问题的解
}

实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
1、先找到最小问题规模时的求解方法
2、考虑随着问题规模增大时的求解方法
3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。

其中|P|表示问题 P 的规模;n0 为一阈值,表示当问题 P 的规模不超过 n0 时,问题已容易直接解出,不必再继续分解。ADHOC§是该分治法中的基本子算法,用于直接解小规模的问题 P。

因此,当 P 的规模不超过 n0 时直接用算法 ADHOC§求解。算法 MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将 P 的子问题 P1 ,P2 ,…,Pk 的相应的解 y1,y2,…,yk 合并为 P 的解。

适用范围

(1)二分搜索
(2)大整数乘法
(3)Strassen 矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔

计算 3 的 2000 次方

123 345 678 * 3 = 370 037 034 在这里我们可以这样写:

123 _ 3 = 369 345 _ 3 = 1035 678 * 3 = 2034

组合在一起是 369 1035 2034

快速幂

int QucikPow(int a, int n)
{
    int ans = 1;
    while (n)
    {
        if (n & 1)        //如果n的当前末位为1
            ans *= a;  //ans乘上当前的a
        a *= a;        //a自乘
        n >>= 1;       //n往右移一位
    }

    return ans;
}

二分搜索

正常二分将一个完整的区间分成两个区间,两个区间本应单独找值然后确认结果,

但是通过有序的区间可以直接确定结果在那个区间,所以分的两个区间只需要计算其中一个区间,然后继续进行一直到结束

public int searchInsert(int[] nums, int target)
{
    if(nums[0]>=target)
        return 0;//剪枝
    if(nums[nums.length-1]==target)
        return nums.length-1;//剪枝
    if(nums[nums.length-1]<target)
        return nums.length;
    int left = 0, right = nums.length - 1;
    while (left < right)
    {
        int mid = (left + right) / 2;
        if (nums[mid]==target)
            return mid;
        else if (nums[mid] > target)
        {
            right=mid;
        }
        else
        {
            left=mid+1;
        }
    }
    return left;
}

快速排序

public void quicksort(int [] a,int left,int right)
{
    int low = left;
    int high = right;

    //下面两句的顺序一定不能混,否则会产生数组越界!!!very important!!!
    if (low > high)//作为判断是否截止条件
        return;
    int k=a[low]; //额外空间k,取最左侧的一个作为衡量,最后要求左侧都比它小,右侧都比它大。

    while (low < high) //这一轮要求把左侧小于a[low],右侧大于a[low]。
    {
        while(low < high && a[high] >= k)//右侧找到第一个小于k的停止
        {
            high--;
        }

        //这样就找到第一个比它小的了
        a[low] = a[high];   //放到low位置

        while (low < high && a[low] <= k) //在low往右找到第一个大于k的,放到右侧a[high]位置
        {
            low++;
        }
        a[high] = a[low];
    }
    a[low] = k;//赋值然后左右递归分治求之
    quicksort(a, left, low-1);
    quicksort(a, low+1, right);
}

归并排序(逆序数)

归并在分的时候按照数量均匀分,而合并时候已经是两两有序的进行合并的,因为两个有序序列 O(n)级别的复杂度即可得到需要的结果。

而逆序数在归并排序基础上变形同样也是分治思想求解。

请添加图片描述

private static void mergesort(int[] array, int left, int right)
{
  int mid = (left + right) / 2;
  if (left < right)
  {
    mergesort(array, left, mid);
    mergesort(array, mid+1, right);
    merge(array, left,mid, right);
  }
}

private static void merge(int[] array, int l, int mid, int r)
{
  int lindex = l;
  int rindex = mid + 1;
  int team[] = new int[r-l+1];
  int teamindex=0;
  while (lindex<=mid&&rindex<=r) {//先左右比较合并
    if(array[lindex] <= array[rindex])
    {
        team[teamindex++] = array[lindex++];
    }
    else
    {
        team[teamindex++] = array[rindex++];
    }
  }
  while(lindex <= mid)//当一个越界后剩余按序列添加即可
  {
    team[teamindex++] = array[lindex++];

  }
  while(rindex <= r)
  {
    team[teamindex++] = array[rindex++];
  }
  for(int i = 0;i < teamindex; i++)
  {
    array[l + i] = team[i];
  }
}

最近点对

题目描述

在二维坐标轴上有若干个点坐标,求出最近的两个点的距离

请添加图片描述

如果直接分成两部分分治计算你肯定会发现如果最短的如果一个在左一个在右会出现问题。我们可以优化一下。

在具体的优化方案上,按照 x 或者 y 的维度进行考虑,将数据分成两个区域,先分别计算(按照同方法)左右区域内最短的点对。

然后根据这个两个中较短的距离向左和向右覆盖,计算被覆盖的左右点之间的距离,找到最小那个距离与当前最短距离比较即可。

请添加图片描述

struct point {
	double x;
	double y;
	point(double x, double y) :x(x), y(y) {}
	point() { return; }
};

bool cmp_x(const point & A, const point & B)  // 比较x坐标
{
	return A.x < B.x;
}

bool cmp_y(const point & A, const point & B)  // 比较y坐标
{
	return A.y < B.y;
}

double distance(const point & A, const point & B)
{
	return sqrt(pow(A.x - B.x, 2) + pow(A.y - B.y, 2));
}


/*
* function: 合并,同第三区域最近点距离比较
* param: points 点的集合
*        dis 左右两边集合的最近点距离
*        mid x坐标排序后,点集合中中间点的索引值
*/
double merge(vector<point> & points, double dis, int mid)
{
	vector<point> left, right;
	for (int i = 0; i < points.size(); ++i)  // 搜集左右两边符合条件的点
	{
		if (points[i].x - points[mid].x <= 0 && points[i].x - points[mid].x > -dis)
			left.push_back(points[i]);
		else if (points[i].x - points[mid].x > 0 && points[i].x - points[mid].x < dis)
			right.push_back(points[i]);
	}

	sort(right.begin(), right.end(), cmp_y);

	for (int i = 0, index; i < left.size(); ++i)  // 遍历左边的点集合,与右边符合条件的计算距离
	{
		for (index = 0; index < right.size() && left[i].y < right[index].y; ++index);
		for (int j = 0; j < 7 && index + j < right.size(); ++j)  // 遍历右边坐标上界的6个点
		{
			if (distance(left[i], right[j + index]) < dis)
				dis = distance(left[i], right[j + index]);
		}
	}
	return dis;
}


double closest(vector<point> & points)
{
	if (points.size() == 2) 
		return distance(points[0], points[1]);  // 两个点
	if (points.size() == 3) 
		return min(distance(points[0], points[1]),min(distance(points[0], points[2]),
		distance(points[1], points[2])));  // 三个点
		
	int mid = (points.size() >> 1) - 1;
	double d1, d2, d;
	
	vector<point> left(mid + 1), right(points.size() - mid - 1);
	copy(points.begin(), points.begin() + mid + 1, left.begin());  // 左边区域点集合
	copy(points.begin() + mid + 1, points.end(), right.begin());  // 右边区域点集合
	d1 = closest(left);
	d2 = closest(right);
	d = min(d1, d2);
	
	return merge(points, d, mid);
}

int main()
{
	int count;
	printf("点个数:");
	scanf("%d", &count);
	vector<point> points;
	double x, y;
	for (int i = 0; i < count; ++i)
	{
		printf("第%d个点", i);
		scanf("%lf%lf", &x, &y);
		point p(x, y);
		points.push_back(p);
	}
	sort(points.begin(), points.end(), cmp_x);
	printf("最近点对值:%lf", closest(points));
	return 0;
}

Honi

完成汉诺塔需要的步数

StepNum = (int)pow(2,num) - 1; // num为塔数
#include <iostream>
using namespace std;

int count = 0;
void honi(int n, char a, char b, char c)
{
    count++;
    if (n == 1)
    {
        cout << a <<"-->" << c<<endl;
    }
    else
    {
        honi(n - 1, a, c, b);
        cout << a << "-->" << c<<endl;
        honi(n - 1, b ,a, c);
    }
}

int main()
{
    int n;
    char a, b, c;
    cin >> n;
    honi(n,'A','B','C');
    cout << count <<endl;
    return 0;
}

士兵站队

题目描述

在一个划分成网格的操场上, n n n 个士兵散乱地站在网格点上,由整数坐标 ( x , y ) (x,y) (x,y) 表示。

士兵们可以沿网格边上、下左右移动一步,但在同时刻任一网格点上只能有 1 名士兵。

按照军官的命令,他们要整齐地列成一个水平队列,即排成队列,即排成 ( x , y ) , ( x + 1 , y ) , … , ( x + n − 1 , y ) (x,y),(x+1,y),\ldots,(x+n-1,y) (x,y),(x+1,y),,(x+n1,y)。请求出如何选择 x x x y y y 的值才能使士兵们以最少的总移动步数排成一列。

纵坐标

y 取中位数,才使得纵坐标相加和最少
n 个士兵的 Y 轴坐标分别为:
Y0,Y1,Y2 …… …… Yn-1
则最优步数 S=|Y0-M|+|Y1-M|+|Y2-M|+ …… …… +|Yn-1-M|
结论:M 取中间点的值使得 S 为最少(最优)

横坐标

首先需要对所有士兵的 X 轴坐标值进行排序
然后,按从左至右的顺序依次移动到每个士兵所对应的“最终位置”(最优),所移动的步数总和就是 X 轴方向上需要移动的步数

例,最左的士兵移动到“最终位置”的最左那位,第二个士兵移动到“最终位置”的第二位
则总的步数为:士兵一移动步数+士兵二移动步数+ …… +士兵 n 移动步数
如何确定 X 轴方向上的最佳的“最终位置”?

共 n 个士兵
他们相应的 X 轴坐标为:X0,X1,X2 …… …… Xn-1
设,士兵需要移动到的“最终位置”的 X 轴坐标值为:k,k+1,k+2 …… …… k+(n-1)
则所求最优步数 S=|X0-k|+|X1- (k+1) |+|X2-(k+2)|+ …… +|Xn-1-(k+(n-1))|
经过变形 S=|X0-k|+|(X1-1)-k|+|(X2-2)-k|+ …… …… +|(Xn-1-(n-1))-k|

注意到公式的形式与 Y 轴方向上的考虑一样,同样是 n 个已知数分别减去一个待定数后取绝对值
因此还是采用取中位数的办法求得 k 值,最后算出最优解

代码

#include <bits/stdc++.h>
using namespace std;

int i, j, n;

int main()
{
    cin >> n;
    int x[n], y[n],xpath = 0, ypath = 0;

    for (i = 0; i < n; i++) {
        cin>> x[i] >> y[i];

    }
    sort(x, x + n);
    sort(y, y + n);

   for (i = 0; i < n / 2; i++)
   {
       ypath += y[n - 1 - i] - y[i];
   }

    for (i  = 0; i < n; i++)
    {
        x[i] = x[i] - i;
    }
    sort(x, x + n);
    for (i  = 0; i < n / 2;i++)
    {
        xpath += x[n - i - 1] - x[i];
    }
    cout << xpath + ypath;
    return 0;
}

子数组的最大平均值

题目描述

给出一个整数数组,有正有负。找到这样一个子数组,他的长度大于等于 k,且平均值最大.

分治法

比较左、右、中间三部分的序列和的大小,因为中间部分是没办法分治的,只能在每一层递归函数空间里面进行,所以递归的部分为左、右,而且左右部分序列和有分别为次层递归的结果。

递归结束的标志:左右为相同位置元素,即只有一个元素.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分割的思想跟二叉树的遍历大同小异,能左分,不右分,分到当前序列中只有一个值时,改值作为最大的序列和返回。

int getMaxNum(int a, int b, int c)
{
    if (a > b && a > c)
    {
        return a;
    }
    if (b > a && b > c)
    {
        return b;
    }
    return c;
}
int maxSumRec(int data[], int left, int right)
{
    if (right - left == 1)
    {
        // 如果当前序列只有一个元素
        return data[left];
    }
    int center = (left + right) / 2; // 计算当前序列的分裂点
    int maxLeftSum = maxSumRec(data, left, center);
    int maxRightSum = maxSumRec(data, center, right);
    // 计算左边界最大子序列和
    int leftBonderSum = 0;
    int maxLeftBonderSum = data[center - 1];
    for (int i = center - 1; i >= left; i--)
    {
        leftBonderSum += data[i];
        if (maxLeftBonderSum < leftBonderSum)
        {
            maxLeftBonderSum = leftBonderSum;
        }
    }
    // 计算右边界最大子序列和
    int rightBonderSum = 0;
    int maxRightBonderSum = data[center];
    for (int i = center; i < right; i++)
    {
        rightBonderSum += data[i];
        if (maxRightBonderSum < rightBonderSum)
        {
            maxRightBonderSum = rightBonderSum;
        }
    }
    // 返回当前序列最大子序列和
    return getMaxNum(maxLeftBonderSum + maxRightBonderSum, maxLeftSum, maxRightSum);
}

二分答案

我们考虑二分平均值,那么我们需要一个 ​check​ 函数,能在 O(N)复杂度内判断是否存在一个子数组的平均值大于等于我们二分出来的平均值

对于一个平均数 ​ave​,我们先将 ​nums​ 数组每个数减去 ​ave​,那么只要存在一个长度大于 ​k​ 的子数组和大于等于 0,就说明平均数 ​ave​ 可行,这可以在 O(N)时间内完成

  1. 设置二分的左右边界分别为数组中的最小值和最大值
  2. 判断平均值 ​mid​ 是否可行,若可行则说明答案大于等于 ​mid​,那么左边界等于 ​mid​,否则说明答案小于 ​mid​,右边界等于 ​mid​
    如何判断平均值 ​mid​ 是否可行:
    ~ 将 ​nums​ 数组每个数减去 ​mid​

1… 求 ​nums​ 数组的前缀和数组 ​pre​ 2. 设置指针 ​index​ 等于 k 3. 那么在 ​nums[0:index]​ 中,长度大于 ​k​ 的子数组,区间和最大为 ​pre[index - 1] - min{pre[0 : index - k]}​ 4. 将 ​index​ 不断右移直到指向数组末端,若中间区间和最大值大于等于 ​0​,​check​ 函数直接返回 ​True​,结束后还为返回值则返回 ​False​ 5. 不断重复 2 直到 ​left + 1e-5 == right​ 退出 6. 返回左边界

public class Solution {

    /**
     * @param nums: an array with positive and negative numbers
     * @param k: an integer
     * @return: the maximum average
     */

    private boolean check(int[] nums, int k, double avg) {

        //rightSum表示当前指向位置的前缀和
        //leftSum表示当前指向位置左侧k个位置的前缀和
        //minLeftSum表示左侧最小的前缀和

        double rightSum = 0, leftSum = 0, minLeftSum = 0;
        for (int i = 0; i < k; i++) {
            rightSum += nums[i] - avg;
        }
        for (int i = k; i <= nums.length; i++) {
            if (rightSum - minLeftSum >= 0) {
                return true;
            }
            if (i < nums.length) {
                rightSum += nums[i] - avg;
                leftSum += nums[i - k] - avg;
                minLeftSum = Math.min(minLeftSum, leftSum);
            }
        }
        return false;
    }

    public double maxAverage(int[] nums, int k) {
        double left, right, mid;

        //设置二分的左右边界分别为数组中的最小值和最大值

        left = right = nums[0];
        for (int i = 0; i < nums.length; i++) {
            left = Math.min(nums[i], left);
            right = Math.max(nums[i], right);
        }
        while (left + 1e-5 < right) {
            mid = left + (right - left) / 2;

            //判断平均值mid是否可行
            //若可行则说明答案大于等于mid,那么左边界等于mid
            //否则说明答案小于mid,右边界等于mid

            if (check(nums, k, mid)) {
                left = mid;
            }
            else {
                right = mid;
            }
        }
        return left;
    }
}

全排列

题目

用交换的分治法实现前 m(m<10)个自然数数的全排列。
提示:通过交换实现的全排列不是字典序的全排列。

思路

去掉序列的第一个位置的元素(当然可以是任意一个位置),剩下的元素进行全排列,

由于每一个元素都可以占用第一个元素的位置,那么在第一个位置枚举所有的元素,就可以覆盖所有的全排列情形。

代码

//生成列表a的全排列
//输入:一个全排列元素列表a[0..n-1]
//输出:a的全排列集合
void perm(int a[],int s,int n)
{
    //解决规模最小的子问题:只有一个元素的序列的全排列
    if(s == n-1)
    {
        for(i = 0;i < n;i++)
            cout << a[i] << " ";
        cout<< endl;
    }
    else//否则按照递推式划分子问题
    for(i = s; i < n;i++)
    {
        swap(a[s], a[i]);
        perm(a, s+1, n);
        swap(a[s], a[i]);
    }
    return;
}

  • 8
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值