杭电acm入门

第一讲 数学基础

before: 多组输入的处理

输入处理

  • 类型一:输入不说明有多少input block,默认以**EOF**为读入结束

例1:A+B Problem

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int a, b;
while(scanf("%d %d", &a, &b) != EOF)
printf("%d\n", a + b);
return 0;
}

scanf函数的返回值为成功输入的变量个数,无读入返回**EOF(为-1**,而非0),因而我们还可以这样:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int a, b;
while(scanf("%d %d", &a, &b) == 2)
printf("%d\n", a + b);
return 0;
}

若使用cpp,更加方便

1
2
3
while(cin >> a >> b) {
...
}
  • 类型二:输入开始告知有N个Input Block

先读入N,在借助while或者for循环实现

  • 类型三:不说明有多少Input Block,但是以特殊输入为结束标志

处理方式类似类型一——先读

仍然以A+B Problem为例,当a与b均不为0时结束

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int a, b;
while(scanf("%d %d", &a, &b) != EOF && (a != 0 || b != 0))
printf("%d\n", a + b);
return 0;
}

更好地:(可读性更好)

1
2
3
4
while(scanf("%d %d", &a, &b)) {
if(a == 0 && b == 0) break;
printf("%d\n", a + b);
}
  • 类型四:前三种的组合问题

  • 类型五:字符串的输入问题

例:HDOJ_1048

若c:

1
2
3
char buf[20];
gets(buf);
// 不安全的读入,不推荐!

若cpp:

1
2
3
4
//如果用 string buf; 来储存
getline(cin, buf);
//如果用 char buf[255]; 来保存
cin.getline(buf, 255); //cin的成员函数

输出处理

  • 类型一:每个Input Block对应一个Output Block,Output Block之间没有空行

  • 类型二:每个Input Block对应一个Output Block,Output Block之后有一个空行

  • 类型三:每个Input Block对应一个Output Block,Output Block之间有一个空行

小差别注意即可

注意

多组数据,是否需要一次性全部读取,再一组一组处理?

不需要

提交到OJ的程序从文件中读取,然后生成临时文件存储输出,与标准Output进行逐字符对比处理

导引:整数求和(N500000N \leq 500000)

  • 等差数列的公式求和的风险?

    sum(n)=n×(n1)/2sum(n) = n \times (n - 1) / 2,N过大时,n×(n1)n \times (n-1)会爆`int`(32位)

    解决方案:

    • 64位整数(long long, %lld)

例一:求最小公倍数

  • 暴力枚举?TLE

  • 数学方法:LCM(A,B)=A×B/GCD(A,B)LCM(A,B)=A \times B/GCD(A,B)

    注意点同导引,如何防止过程中爆int?先÷后x

    LCM(A,B)=A/GCD(A,B)×BLCM(A,B)=A/GCD(A,B) \times B
  • 求GCD?辗转相除法(欧几里得算法)

    • O(logn)O(\log n)级的算法
    • 原理
    • 终止条件:有一方为0,另一方为gcd
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int gcd(int a, int b) {
    int temp;
    while(b != 0) {
    temp = a % b;
    a = b;
    b = temp;
    }
    return a;
    }

    ​ 为什么不需要在意大小?第一轮处理会交换

    1
    2
    3
    //递归写法
    int gcd(int a, int b)
    return (b != 0) ? gcd(b, a % b) : a;

例二:计算N个N相乘的个位数——规律

  • 1N1,000,000,0001 \leq N \leq 1,000,000,000)
  • 暴力(TLE)
  • 每次处理后模10?——解决了数据溢出,但是循环次数仍然太多
  • 找到规律循环节,后打表

为什么去找规律?

  • 数据规模太大
  • 暴力计算不可行

例三:特别的fibonacci数列——规律与循环节

F(0)=7F(0) = 7 F(1)=11F(1) = 11 F(n)=F(n1)+F(n2)(n>3)F(n) = F(n-1)+F(n-2)\quad(n > 3)

给定一个n(n<1000000),请判断F(n)能否被3整除,分别输出yes和no

  • 递归计算每一项,模3 数据规模太大

  • 同余定理F(n)%3=F(n1)%3+F(n2)%3F(n)\%3=F(n-1)\%3+F(n-2)\%3

  • 规律:发现循环节

    G(1) G(2) G(3) G(4) G(5) G(6) G(7) G(8) G(9) G(10) G(11)
    1 2 0 2 2 1 0 1 1 2 0

    循环节为8

  • 为什么一定会有循环节?

    ——抽屉原理

    由于%3结果一定是0 1 2,且前两项导出后一项,考虑所有相邻两项的组合,共有9种组合,且0 0情况可排除,即8种,则根据抽屉原理(容斥原理),则循环节在大致9位内会出现,一旦出现了重复的相邻两项,则一定出现了循环节

例四:求A^B最后三位数表示的整——快速幂

  • 找规律不可行 样本太大了

  • 快速幂(二分加速)

    • 递归实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      int power(int a, int n) {
      int ans;
      if(n == 0)
      ans = 1;
      else {
      ans = power(a * a, n / 2);
      if(n % 2 == 1)
      ans *= a;
      }
      return ans;
      }
    • 非递归实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      int power(int a, int n) {
      int ans = 1;
      while(n) {
      if(n % 2)
      ans *= a;
      a = a * a;
      n /= 2;
      }
      return ans;
      }
    • 特别注意:小心爆**int**!!

    • 快速幂基本都伴随着取模运算

第二讲 贪心算法

  • 总是做出当前来看的最优解
  • 分解子问题,对子问题按照同样的策略进行操作

例一:田忌赛马

已知田忌与国王所有马的能力值,赢的人获得200元,如何找到最优解?

  • 贪心策略

    用自己的马挑战对方比自己弱的最小的马

  • 确定思路

    • 排序
    • 比较未标记的最好的马,自己的能超过对方的马吗
      • 如果可以,则赛马
      • 如果不可以,则用自己最差的马对方最好的马赛马
      • 赛马做标记
    • 重复策略直至所有的马比完

例二:事件序列问题

  • 贪心策略

    结束时间排序,尽量留出更多的时间

  • 确定思路

    • 提出命题:至少存在一个最长的事件序列,包含最早结束的事件

      • 证明:反证法

        逆命题所有的最长事件序列都不包含最早结束的事件

        在该假设下任取一个最长的事件序列,将第一个事件与最早结束的事件替换,则构造了一个相同长度的最长的时间序列且含有最早结束的事件,逆命题不成立,原命题得证

    • 按照结束时间排序

    • 找到最早结束事件作为第一个事件

      此时问题分解为n-1阶的子问题,注意额外分析是否会冲突

    • **注意!!**选出的结果方案不唯一,而方案的序列长度一定唯一

例三:搬桌子

丁爸信奥培训中心最近在富丽科技大厦租了一层楼,这层楼的形状如下: img

由图可见,这层楼中间是走廊,两侧各有200个房间,编号如上图。

最近,丁爸信奥培训中心做了内部机构的调整,需要把一些桌子从一个房间搬到另外的房间。因为走廊很窄,但是桌子很大,所以同一段走廊每次只能通过一个桌子。 假设不论远近,每趟搬桌子都需要10分钟。同时,当你从房间i搬桌子到房间j的过程中,房间i到房间j之间的走廊都被占用,也就是说,在每个10分钟内,不能有多个任务共享同一段走廊。

现在,丁爸想知道:要完成所有的搬运任务,最少需要多少时间?

  • 问题分析:

    • 为什么不能找多次最长事件序列?

      最长事件序列只能保证单一最优解,全局并非最优解

  • 贪心策略

    • 最优策略:记录路径,找到重叠最多地方的重叠次数,即为所需最多的次数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      #include<iostream>
      #include<algorithm>
      #include<cstring>
      using namespace std;
      int p[201];
      int main() {
      int T, ans;
      cin >> T;
      while(T --) {
      int n;
      cin >> n;
      int s,t;
      memset(p, 0, sizeof(p));
      ans = 0;
      for(int i = 0; i < n; i ++) {
      cin >> s >> t;
      s = (s + 1) / 2;
      t = (t + 1) / 2;
      if(s > t)
      swap(s, t);
      for(int j = s; j <= t; j++)
      p[j]++;
      }
      for(int i = 1; i <= 200; i ++)
      if(p[i] > ans)
      ans = p[i];
      cout << ans * 10 << endl;
      }
      return 0;
      }
    • 较差策略:按照开始位置排序,从第一个需要搬的桌子开始i,一次性找到所有线路不冲突的桌子,不断分解子问题

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      #include<stdio.h>
      #include<vector>
      #include<iterator>
      #include<algorithm>
      using namespace std;
      struct M{
      int s;
      int t;
      };

      bool comp(const M &m1, const M &m2){
      return m1.s < m2.s; //按照开始的房间号排序
      }

      int main(){
      int T, s, t;
      scanf("%d", &T);
      while(T--) {
      int n;
      vector<M> v;
      scanf("%d", &n);
      for(int i = 0; i < n; ++i) {
      M m;
      scanf("%d%d", &s, &t);
      if(s > t)//有可能从号码大的房间往回搬,若是,则交换s与t
      swap(s, t);
      m.s = s;
      m.t = t;
      v.push_back(m);
      }
      sort(v.begin(), v.end(), comp);
      int ans = 0;
      for(vector<M>::iterator it = v.begin(); it != v.end();){ //从房间号最小的开始查找
      ++ans;
      int t = (*it).t;
      for(vector<M>::iterator subit = it; subit != v.end();) { //找到与要搬的线路不冲突的桌子
      if((*subit).s > t && !((*subit).s - t == 1 && (*subit).s % 2 == 0)) { //注意对面房间的线路也是冲突的,比如5与6
      t = (*subit).t;
      subit = v.erase(subit); //该桌子已搬完,删除
      } else
      ++subit; //否则继续查找下一张桌子
      }
      it = v.erase(it); //删除
      }
      printf("%d\n", ans * 10);
      }
      return 0;
      }

例四:删数问题

有一个长度为n(n <= 240)的正整数,从中取出k(k < n)个数,使剩余的数保持原来的次序不变,求这个正整数经过删数之后最小是多少。

  • 子问题分解:每一次删除数字都使得剩下的正整数保持最小

  • 贪心策略:

    删除从左向右第一个逆序列的第一个数字

    或 删除从左到右递增序列结束处的数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<bits/stdc++.h>
using namespace std;

int main() {
string str;
int k, flag = 0;
vector<char> v;

cin >> str >> k;

for(int i = 0; i < str.size(); i++){
v.push_back(str[i]);
}

while(k--){
int i = 0;
flag = 0;
vector<char>:: iterator t = v.begin(); //这里主要是为了调用 v.erase()的库函数删除元素
while(i != v.size() - 1) { //注意这里的减一 因为下方的v[i+1] 否则会出现段错误
if(v[i] > v[i + 1]) { //如果出现后一个数小于前一个数那么这就是这一趟的递增的终点
v.erase(t);
flag = 1;
break;
}
i++;
t++;
}

if(flag == 0) {//如果是一个递增序列那么的话就要删除最后一个数
v.erase(t);
}
}
int i = 0;
//这么输出是为了防止前置'0'的输出
for(int i = 0; i < v.size(); i++) {
if(v[i] != '0')
break;
}
for(int j = i; j < v.size(); j++){
cout << v[j];
}
}

例五:青蛙的邻居

未名湖附近共有N个大小湖泊L1, L2, …, Ln(其中包括未名湖),每个湖泊Li里住着一只青蛙Fi(1 ≤ i ≤ N)。如果湖泊Li和Lj之间有水路相连,则青蛙Fi和Fj互称为邻居。现在已知每只青蛙的邻居数目x1, x2, …, xn,请你给出每两个湖泊之间的相连关系。

Input

第一行是测试数据的组数T(0 ≤ T ≤ 20)。每组数据包括两行,第一行是整数N(2 < N < 10),第二行是N个整数,x1, x2,…, xn(0 ≤ xi ≤ N)。

Output

对输入的每组测试数据,如果不存在可能的相连关系,输出"NO"。否则输出"YES"

  • 离散数学可图形判定

    • 度序列

      若把一个图G所有顶点的度数台城一个序列S,则称S为图G的度序列

    • 度序列是可图

      一个非负整数组成的有限序列如果是某个无向图的度序列,则称该序列是可图的

    • Havel-Hakimi定理

      非负整数组成的有限非递增序列S={d1,d2,d3...dn}S=\{d1,d2,d3...dn\}当且仅当S1={d21,d31...d(d1+1),d(d1+2)......dn}S1=\{d2-1,d3-1...d(d1+1),d(d1+2)......dn\}也是可图的

  • 贪心策略

    通过havel-hakimi定理,对序列由大到小排序,然后对S1S_1操作,排序,对S2S_2操作,排序……直到序列全部变成0。如果出现负数,则该度序列是不可图的

第三讲 并查集

导引问题

在某个城市里住着n个人,现在存在关于n个人的m条信息,假设所有认识的人为一个单位,最多有多少个单位

什么是并查集

  • Disjoint Set,即“不相交的集合

    N个元素的N个对象划分为不相交集合,每个集合中选定某个元素代表所在集合

  • 常见两种操作

    • 两个集合
    • 找某个元素属于哪个集合

实现方法一

  • 编号最小的元素标记所在集合

  • 定义一个数组Set[1..n]Set[1..n],其中Set[i]Set[i]表示元素ii所在集合

    Set(i) 1 2 1 4 2 6 1 6 2 2
    i 1 2 3 4 5 6 7 8 9 10

    不相交集合:

    {1,3,7},{4},{2,5,9,10},{6,8}\{1, 3, 7\}, \{4\}, \{2, 5, 9, 10\}, \{6, 8\}
  • 两个功能

    • O(1)O(1)

      1
      2
      int find(x)
      return Set[x];
    • O(n)O(n)

      1
      2
      3
      4
      5
      6
      7
      8
      void Merge(a, b) {
      int i = min(a, b);
      int j = max(a, b);
      for(int k = 1; k <= N; k++) {
      if(Set[k] = j)
      Set[k] = i;
      }
      }

实现方法二——树

  • 利用实现

  • 定义数组Set[1..n]Set[1..n]

    • Set[i]=iSet[i] = i,说明ii表示该集合,是该集合的对应树的
    • Set[i]=jSet[i] = j,说明jjii父节点
  • 两个功能

    • 查 最坏O(n)O(n)

      1
      2
      3
      4
      5
      6
      int find(x) {
      int r = x;
      while(Set[r] != r)
      r = Set[r];
      return r;
      }
    • O(1)O(1)

      1
      2
      void merge(a, b)
      Set[a] = b;
    • 避免出现最坏情况

      • 方法:将深度的树合并到深度的树

      • 实现:假设两棵树的深度h1h_1,h2h_2,则合并后的树的高度为

        h={max(h1,h2)if h1h2h1+1if h1=h2h = \begin{cases} \max(h_1, h_2) & \text{if } h_1 \neq h_2 \\ h_1+1 & \text{if } h_1=h_2 \end{cases}
      • 效果:任何顺序的合并操作后,包含k个节点的树的最大高度不超过logk\log k

    • 优化后

      1
      2
      3
      4
      5
      6
      7
      8
      9
      void merge(a, b) {
      if(height[a] = height[b]) {
      height[a]++;
      Set[b] = a;
      } else if(height(a) < height(b))
      Set[a] = b;
      else
      Set[b] = a;
      }

      此时,查的效率已经变为最坏O(logn)O(\log n)

实现方法三——带路径压缩的查找操作

1
2
3
4
5
int find(x) {
if(Set[x] != x)
Set[x] = find(Set[x]);
return Set[x];
}

例一

经典案例:最小生成树

  • Prim算法

  • Kruskal算法

    • 理论基础——MST性质

      命题:至少存在一颗最小生成树含有权值最小的边

    ​ 证明:类似第二讲例二的证明

    • 算法步骤:(贪心算法)
      • 选中最小的边
      • 找到下一个最小的边,若两个顶点不连通,则选中,重复
      • 直到选中n1n-1条边

例二

第四讲 递推求解

  • 递推关系式状态转移方程

    F(n)F(n)F(n1)F(n-1)与增减量表示 可能与前面的多项有关系

例一——状态产生分析

一个平面上有nn条直线,这些直线中每一条在圆内同其他直线相交,假设没有三条直线相交,试问能分成多少区域

  • 分析:假设此时有n-1条直线分割圆,令第n条直线把上述n-1条直线分割产生n-1个端点,n条线段,即可分割产生n个新区域

初始状态:F(1)=2F(1)=2

状态转移方程:F(n)=F(n1)+nF(n)=F(n-1)+n

例二

平面上有nn条折线,这些折线最多能将平面分割为多少块?

  • 分析:假设此时有n-1条直线分割平面,第n条折线可以做到与前n-1条折线都产生四个交点

    状态转移方程:F(n)=F(n1)+4(n1)+1F(n)=F(n-1)+4(n-1)+1

例三——状态向前分析

2×n2\times n的长方形方格中,用n个1×21\times 2的骨牌铺满方格,有多少种铺设的方案

  • 例一与例二都是从状态产生的角度去思考状态转移方程

  • 然而 例三给出了另一种思考方式

    从第nn状态向前分析成因

    第n列格子分为横竖两种状态

    若为竖,则前有F(n-1)种状态

    若为横,则n列和n-1列为双横,前有F(n-2)种状态,故:

    状态转移方程:F(n)=F(n1)+F(n2)F(n)=F(n-1)+F(n-2)

  • 核心:大问题分类讨论分别解决

例四

1×n1 \times n的长方形,用1×11\times 11×21\times 21×31\times 3的骨牌铺满方格,求第n个多少种铺法

  • 分析同例三

    状态转移方程:F(n)=F(n1)+F(n2)+F(n3)F(n)=F(n-1)+F(n-2)+F(n-3)

总结

  • 首先:确认能否容易得到初始状态的解
  • 假设:规模不大于N1N-1的状态已经得到了解决
  • 最后:重点分析当规模扩大到NN时,如何枚举当前的情况,然后用子问题生成

例五——合法性角度分类

所有学生站成一排,规定女孩不能单独站,即要么队列中没有女生,要么不止一个女生并排站

比如,n=4n=4时,有下面合法队列:

FFFF、FFFM、MFFF、FFMM、MFFM、MMFF、MMMM

给定人数nn求可能的合法队列数

  • 两种做法,一种分男女考虑合法与非法状况
  • 另一种设计子函数分别表示男女来生成
状态转移方程F(n)=F(n1)+F(n2)+F(n4)\text{状态转移方程}\qquad F(n)=F(n-1)+F(n-2)+F(n-4)

例六

有排成一行的nn个方格,用R,P,G三色进行涂色,每格涂一种颜色,要求航信放个不能同色,首位两格也不能同色

  • 依旧是合法性分类

状态转移分析:

  • 前n-1格子序列合法的情况:F(n1)F(n-1) 此时第n个格子不能与合法的第一个和第n-1个颜色相同,只能有一种选择
  • 前n-1格子序列非法的情况:2F(n2)2F(n-2) 若想在第n个格子添加后生成合法的格子序列,该非法类型只能是第一个格子与第n-1个格子同色 这一部分的非法序列来源于合法的n-2序列添加一个首色格子,为F(n2)F(n-2) 此时的最后一个格子有两种颜色可以选择

故:F(n)=F(n1)+2F(n2)F(n)=F(n-1)+2F(n-2)

例七

2×n2\times n个数字按照顺时针方向围成一个圆,nn条线段把所有数字两两相连,要求不能有交叉

求:有多少合法的连线方案

  • 卡特兰数列(Catalan)

    1、2、5、14、642、132,……

    c[0]=1c[0]=1 c[n]=i=0n1c[i]c[n1i]c[n]=\sum_{i=0}^{n-1}c[i]c[n-1-i] c[n]=C2nnn+1\Rightarrow c[n]=\frac{C_{2n}^{n}}{n+1}
  • 高精度组合问题

第五讲 动态规划

典例1

![[Pasted image 20250225215005.png]] 有如上数塔,从顶部出发,在每一个节点都可以选择向左走或者向右走,一直走到底层,要求找到一条路径善意的路径上数值和最大

  • 首先考虑暴力求解,如何遍历所有路径,显然有2n12^{n-1}条路径,不予考虑
  • 分解子问题: 从顶点开始考虑左右两侧的最优解(类似二叉树考虑左右子树的最好情况) 时间复杂度:O(n2)O(n^2)
  • 结论自顶向下的分析,自底向上的计算

例一:免费馅饼

数轴上可能在某一时刻在某一位置掉落免费馅饼,0秒时人在5m处,每秒钟人只能移动一米,给出馅饼掉落的位置与时刻,给出获得馅饼最多的最优解

  • 显然问题仍然在于分解时间寻找最优解,问题是如何分解?
  • 注意到人的操作只有向左或向右或停止不动,那么将一维的数轴按照时间作为第二个轴进行绘图,发现其实就是数塔问题

典例二:最长有序子序列

I 0 1 2 3 4 5 6 7 8
Num[I] 1 4 7 2 5 8 3 6 9
  • 如何分解子问题?
  • 考虑到序列的寻找是从无至有的,也就是可以将问题规模从小到大考虑
I 0 1 2 3 4 5 6 7 8
Num[I] 1 4 7 2 5 8 3 6 9
F[I] 1 2 3 2 3 4 3 4 5
F[I]F[I]为以Num[I]Num[I]为序列结束数字的最大长度,也就是将规模为n的问题分解为n个子问题分别计算,再进行比较即可

核心思想:利用已经解决的子问题作为已知条件进行演算后续问题(即:站在巨人的肩膀上看问题)

例二:胖老鼠问题

给出若干个老鼠的体重和速度,找到一个最长的序列,使得体重越来越大,速度越来越低

  • 按照体重递增排序,然后按照速度降序的原则寻找最长递减有序子序列

例三:最少拦截系统

给出n个飞弹,并且给出这n个飞弹的高度,有拦截系统进行拦截,且拦截系统拦截的多个导弹的高度只能递减

1:一套拦截系统最多可以拦截几个导弹? 仍然是最长不递增有序子序列问题 2:最少需要几套拦截系统才能拦截所有导弹?

  • 尝试模拟?单次都做到拦截最多——贪心算法

  • 问题的本质:最少分成几个”最长不递增有序子序列问题“

    • Dilworth定理: 对于一个偏序集,最少链划分数等于最长反链长度
    • 结论:本体的本质为 求最长上升子序列的长度

例四:搬宿舍

搬宿舍,只能左手一个右手一个,搬东西消耗的能量为两者质量差的平方成正比。 已知有n个物品,需要搬k次,分别给出所有的物品重量,求消耗的最少能量

  • 第一感觉:两手的东西质量尽可能接近
  • 粗略思考验证:搬运一次必须要搬运质量相邻的两者
  • 预处理:排序
  • 问题的分解:搬运k次的最优解建立前k-1次的最优解情况,考虑递推,考虑dp
  • F(n.k)F(n.k)为在前n个物品中搬运k次,则可以分解问题
F(n,1)=min{(xnxn1)2F(n1,1)F(n,1)=\min \begin{cases} (x_n-x_{n-1})^2\\ F(n-1,1) \end{cases}
  • 如果拓展到F(n,k)F(n,k)呢? 其实还是在分解问题,搬运第k次有两种,一种是这k次搬走了第n个物品,一种是没有,那么状态转移方程按照上述的思路即可解决:
F(n,k)={(xnxn1)2+F(n2,k1)F(n1,k)F(n,k)=\begin{cases} (x_n-x_{n-1})^2+F(n-2,k-1)\\ F(n-1,k) \end{cases}
  • 注意问题的初始状态:问题规模在F(4,2)F(4,2)以下的都可以作为初始状态,F(4,2)F(4,2)的解显然为(x1x2)2+(x3x4)2(x_1-x_2)^2+(x_3-x_4)^2

小结:

  • 基本思想:如果各个子问题不是独立的(即:重复的),不同的子问题的个数只是 多项式量级 (即:有限的),如果我们能用 数组 来保存已解决的子问题的答案,在需要时直接获取已求得的答案,这样就可以避免大量的重复计算
  • 特点:
    • 最优子结构 大问题的最优解一定包含子问题的最优解
    • 重叠子问题 子问题是重叠的,即大规模问题的解决牵扯到小规模问题
    • 无后效性 完成小规模问题后再完成大规模问题的时候,大规模问题的解决不会影响小规模的问题

第六讲 背包问题

导引问题

现有一张餐券面值10元,有菜肴N种,餐券一次性使用不可找零,问如何选择菜肴使得餐券使用最不浪费

什么是背包问题

  • 给定容量VV的背包和若干物品,在一定的限制条件下问最多能放进多少价值的物品
  • 最经典的DP问题
  • 背包问题中状态的理解(状态状态转移

背包问题分类

  • 01背包(非常重要!!!)
  • 完全背包
  • 多重背包
  • 二维费用背包
  • 混合三种背包
  • 分组背包
  • 有依赖的背包

01背包

  • 特点:每种物品仅有一个
  • NN个物品和一个容量为VV的背包,第ii件物品的费用是c[i]c[i],价值是w[i]w[i],求解放入哪些物品使得价值总和最大
  • 问题特点:每种物品仅有一件,仅能选择放or不放
  • 子问题分解? —— 基于状态
  • 无须→有序? 从 11ii 依次考虑各个物品
  • 状态定义:f[i][v]f[i][v]表示考虑前ii件物品后放入一个容量为vv的背包可以获得的最大价值

例一

张三喜欢收集骨头,每个骨头有自己的价值和体积,求问背包可以装下最多价值的物品 例如:体积为10,有5块骨头,价值分别为1,2,3,4,5,体积分别为5,4,3,2,1

  • 仍然采取 第五讲例四 的思路
  • 从前0个物品依次考虑前 ii 个物品,按照这个顺序从背包体积为0到10依次进行分析
    1. 没装第ii个物品——价值为d[i1][v]d[i-1][v](继承上一行的价值)
    2. 装第nn个物品——此时的背包容量相比上一行能否放得下?如果放得下,放入并计算价值
  • 以上两个情况取 max 记录这个格子内
  • 得到了下面的表格(程序的执行过程即为从第一行到最后一行一次从左向右遍历
(Y,C)(Y,C) 0 1 2 3 4 5 6 7 8 9 10
0 0 0 0 0 0 0 0 0 0 0 0
(1,5)(1,5) 0 0 0 0 0 1 1 1 1 1 1
(2,4)(2,4) 0 0 0 0 2 2 2 2 2 3 3
(3,3)(3,3) 0 0 0 3 3 3 3 5 5 5 5
(4,2)(4,2) 0 0 4 4 4 7 7 7 7 9 9
(5,1)(5,1) 0 5 5 9 9 9 12 12 12 12 14
  • 二维背包最能反映01背包的本质——考虑前1个,前2个,前3个……前i个,当背包体积最大时,得到的结果就是最优解
  • 状态转移方程:
DP[i][j]=max(DP[i1][j],DP[i1][jv[i]]+w[i])DP[i][j]=\max(DP[i-1][j],DP[i-1][j-v[i]]+w[i])
  • 时间复杂度与空间复杂度都为 N×VN \times V
  • 空间复杂度的优化:使用一维数组DP[j],从后向前遍历
    1
    2
    3
    for i = 1 to n
    for j = V to v[i]
    dp[j] = max(dp[j], dp[j-v[i]] + w[i])

完全背包

  • 特点:一种物品可以取无数个
  • 可否转化为01背包?

多重背包

  • 二进制优化
1
2
3
4
5
6
7
8
9
10
11
12
//预处理
int t = 1;
while(x >= t) {
v[cnt] = a * t;
c[cnt++] = b * t;
x -= t;
t <<= 1; //按照2^k打包
}
if(x) { //剩余打包
v[cnt] = a * x;
c[cnt++] = b * x;
}

二维费用背包

image.png

第七讲 BFS入门

  • 预备知识 队列 stl queue

层次遍历

  • 二叉树 image.png

image.png

  • BFS标准套路 image.png

第八讲 DFS入门

导引 递归

  • 特征:
    • 先写出口(不需递归的特殊情况)
    • 再写普遍情况(递归解决)

例一 全排列

  • 递归特征:
    • 如果第一个数确定,则剩余问题就是其余 n1n-1 个数字的全排列
    • 如果前 kk 个数确定,则剩余问题就是其余 nkn-k 个数字的全排列
    • 枚举当前的每一种情况
      • 在每一种可能中,递归调用dfs(step + 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<bits/stdc++.h>
using namespace std;
int vis[10], num[10];
int n;

void dfs(int step) {
if(step == n + 1) {
for(int i = 1; i <= n; i++)
cout << a[i] << " ";
cout << endl;
}
for(int i = 1; i <= n; i++) {
if(vis[i] == 0) {
num[step] = i;
vis[i] = 1;
dfs(step + 1);
vis[i] = 0; //回溯
}
}
}

int main() {
while(cin >> n) {
memset(vis, 0, sizeof(vis));
dfs(1);
}
return 0;
}

例二 迷宫搜索

第八讲 二分匹配

  • 二分图 image.png

经典算法:匈牙利算法

变式一:最小定点覆盖

image.png