【刷题】CSP-S/NOIP提高组 真题题解总结 & 近20年题目

PS:整理近20年CSP-S/NOIP提高组 的真题,提供思路和总结。

建议看完思路后自己实现代码,锻炼代码能力~

如需转载请联系我~

作者个人资料


DP:

  • 线性dp

    • P1091 [NOIP2004 提高组] 合唱队形

      比较简单的一道题。求出以 i 结尾的最长上升子序列和以 i 为头的最长下降子序列,相加 1 即可。

    • P1052 [NOIP2005 提高组] 过河

      如果不考虑 L 的范围,那么就是一道简单的线性 dp 。

      但是 L 很大,石头数量很少,所以每相邻两个石头的空隙一定很大。

      前置知识:P3951 [NOIP2017 提高组] 小凯的疑惑

      给定两个数 pq ,他们最大不能凑出的数是 (p1)(q1)1 。所以中间空隙过多一定是可以被凑出来的,即可以压缩中间的空隙,然后线性 dp 即可。

    • P1006 [NOIP2008 提高组] 传纸条

      求出 2 条从 (1,1)(n,m) 的路径。重复只算一次,求最大路径和。

      水题,不多赘述。记录 dpA,B,C,D 表示第一条走到 (A,B) , 第二条走到 (C,D) 。由于 A+B=C+D ,可以记录他们的和,优化掉一维。

    • P1541 [NOIP2010 提高组] 乌龟棋

      记录 dpA,B,C,D 表示用了 A1 号卡片, B2 号卡片, C3 号卡片, D4 号卡片。

      暴力转移即可 FA,B,C,D=max(FA1,B,C,D,FA,B1,C,D,FA,B,C1,D,FA,B,C,D1)+costA+2B+3C+4D

    • P2679 [NOIP2015 提高组] 子串

      前缀和优化线性 dp。(推式子题)

      首先考虑如何DP,然后再考虑如何优化。

      状态表示:fi,j,k表示只用 S 的前 i 个字母,选取了 k 段,可以匹配 T 的前 j 个字母的方案数。

      状态计算:fi,j,k表示的所有方案分成两大类:

      • 不用 Si,则方案数是 fi1,j,k
      • 使用 Si,则方案数是 fit,jt,k1,满足 Sit+1=Tit+1t 从小到大枚举。

      时间复杂度是 O(nm2k)

      我们发先 fi,j,k 的第二项和 fi1,j1,k 很像,可以考虑维护一个 tmpi,j,k

      • Si=Tj , tmpi,j,k=tmpi1,j1,k+fi1,j1,k1
      • SiTjtmpi,j,k=0

      (把式子展开可以发现 tmp 的规律)。

      然后空间会爆,写滚动数组或者 01背包 倒序枚举优化掉第一维即可。

    • P5664 [CSP-S2019] Emiya 家今天的饭

      求解所有方案数

      fi,j 表示前 i 种烹饪方式,做了 j 道菜的方案数。

      • 状态转移:
        i 种烹饪方式不做 fi,j+=fi1,j

      • i 种烹饪方法做 1 道主要食材是 k 的菜:fi,j+=fi1,j1ai,k

      所有方案数量 A=j=1nfn,j

      求解不合法方案:

      dpi,j 表示前 i 中烹饪方法,越界食材数 其他食材数 为 j 的方案数。

      状态转移:

      • i 种烹饪方法不选:dpi,j+=dpi1,j

      • 选越界食材 cdpi,j+=dpi1,j1ai,c

      • 选其他食材 dpi,j+=dpi1,j+1(siai,c)

      所有方案数量: B=dpn,j(j>0)

      AB 即可。

      时间复杂度 O(n2m)

      Tips: 做差有可能为负数,把所有状态加一个 +n 的偏移量即可。

    • 总结

      对于线性 dp的问题,一般状态定义f i , .... ,状态可能很多,所以可能有很多维。

      状态转移:考虑这个集合是由谁构成的,进行分类。比如分成 选 a 和不选 a ......

      优化:如果状态过多,考虑滚动数组或者背包优化,转移中有求和,求最值等考虑用前缀和,单调队列优化。

  • 区间dp

    • P1040 [NOIP2003 提高组] 加分二叉树

      考虑到左边和右边独立,可以想到区间 dp 。

      fi,j 为 区间 [ij] 的最大价值。

      转移fi,j=maxk=ijfi,k1×fk+1,j+wk

      因为要求出具体方案,可以边算边求,也可以算完答案反推回去。

    • P1063 [NOIP2006 提高组] 能量项链

      很模板的一道题。和上一题类似。

      fi,j 为 区间 [ij] 的最大价值。

      转移fi,j=maxk=ijfi,k+fk,j+wi×wj×wk

      套路:枚举长度 l,枚举左端点 i,计算右端点 j,进行转移。

      Tips: 环上问题,破环为链,2 倍长度即可。

    • P1005 [NOIP2007 提高组] 矩阵取数游戏

      考虑到每一行是独立的,可以做 m 次dp

      fi,j 表示将 [ij] 这段数取完的最大值。

      转移

      • 取左端点:fi+1,j+wi×2mj+i

      • 取右端点:fi,j+1+wj×2mj+i

      max 即可。

      需要高精度,附代码:

      #include <bits/stdc++.h>
      using namespace std;
      const int N = 85, M = 31;
      int n, m,w[N][N],f[N][N][M],p[N][M],ans[M];
      void mul(int a[], int b[], int c){
         static int tmp[M];
         for (int i = 0, t = 0; i < M; i ++ ){
             t += b[i] * c;
             tmp[i] = t % 10;
             t /= 10;
         }
         memcpy(a, tmp, M * 4);
      }
      void add(int a[], int b[], int c[]){
         static int tmp[M];
         for (int i = 0, t = 0; i < M; i ++ ){
             t += b[i] + c[i];
             tmp[i] = t % 10;
             t /= 10;
         }
         memcpy(a, tmp, M * 4);
      }
      int compare(int a[], int b[]){
         for (int i = M - 1; i >= 0; i -- )
             if (a[i] > b[i]) return 1;
             else if (a[i] < b[i]) return -1;
         return 0;
      }
      void print(int a[]){
         int k = M - 1;
         while (k && !ans[k]) k -- ;
         while (k >= 0) printf("%d", ans[k -- ]);
         puts("");
      }
      void work(int w[]){
         int a[M], b[M];
         for (int len = 1; len <= m; len ++ )
             for (int i = 0; i + len - 1 < m; i ++ ){
                 int j = i + len - 1;
                 int t = m - j + i;
                 mul(a, p[t], w[i]), mul(b, p[t], w[j]);
                 add(a, a, f[i + 1][j]);
                 if (j) add(b, b, f[i][j - 1]);
                 if (compare(a, b) > 0)
                     memcpy(f[i][j], a, M * 4);
                 else
                     memcpy(f[i][j], b, M * 4);
             }
         add(ans, ans, f[0][m - 1]);
      }
      int main(){
         scanf("%d%d", &n, &m);
         for (int i = 0; i < n; i ++ )
             for (int j = 0; j < m; j ++ )
                 scanf("%d", &w[i][j]);
         p[0][0] = 1;
         for (int i = 1; i <= m; i ++ )
             mul(p[i], p[i - 1], 2);
         for (int i = 0; i < n; i ++ )
             work(w[i]);
         print(ans);
         return 0;
      }
      
    • P7914 [CSP-S 2021] 括号序列

      区间dp & 分类讨论

      状态定义:

      dpi,j 表示从 ij 合法序列数量。

      但是不同的形态可能会有不同的转移。

      将两维的dp扩充为三维,第三维表示不同的形态种类,dp状态就变成了 dpi,j,[0,5]

      • dpi,j,0: 形态如***...*的括号序列(即全部是*)。

      • dpi,j,1: 形态如(...)的括号序列(即左右直接被括号包裹且最左边括号与最右边的括号相互匹配)。

      • dpi,j,2: 形态如(...)**(...)***的括号序列(即左边以括号序列开头,右边以*结尾)。

      • dpi,j,3: 形态如(...)***(...)*(...)的括号序列(即左边以括号序列开头,右边以括号序列结尾,注意:第2种形态也属于这种形态)。

      • dpi,j,4: 形态如***(...)**(...)的括号序列(即左边以*开头,右边以括号序列结尾)。

      • dpi,j,5: 形态如***(...)**(...)**的括号序列(即左边以*开头,右边以*结尾,注意:第1种形态也属于这种形态)。

      i 满足 1in,有 dpi,i1,0=1

      状态转移:

      • dpl,r,0(直接特判)

      • dpl,r,1=(dpl+1,r1,0+dpl+1,r1,2+dpl+1,r1,3+dpl+1,r1,4)compare(l,r)

        1. compare(i,j) 表示第 i 位与第 j 位能否配对成括号,能则为 1,否则为 0
        2. 加括号时,里面可以是全*,可以是有一边是*,也可以是两边都不是*,唯独不能两边都是*且中间有括号序列。
      • dpl,r,2=i=lr1dpl,i,3×dpi+1,r,0

        1. 左边以括号序列开头且以括号序列结尾的是第3种,右边接一串*,是第0种。
      • dpl,r,3=i=lr1(dpl,i,2+dpl,i,3)×dpi+1,r,1+dpl,r,1

        1. 左边以括号序列开头,结尾随便,符合的有第2和第3种,右边接一个括号序列,是第1种。
        2. 记得加上直接一个括号序列的。
      • dpl,r,4=i=lr1(dpl,i,4+dpl,i,5)×dpi+1,r,1

        1. 左边以*开头,结尾随便,符合的有第4和第5种,右边接一个括号序列,是第1种。
      • dpl,r,5=i=lr1dpl,i,4×dpi+1,r,0+dpl,r,0

        1. 左边以*开头,以括号序列结尾,符合的是第4种,右边接一串*,是第0种。
        2. 记得加上全是*的。

      答案: dp1,n,3

      时间复杂度: O(n3)

    • 总结:

      对于区间dp的问题,一般状态定义fi,j 表示区间 [i,j] 的...(最大值最小值等)

      条件 每个区间相互独立,互不影响。

      转移: 外层枚举区间长度,内层枚举起点 i,算出终点 j ,再进行转移。

      Tips: 如果状态里只包含区间不够,则考虑加维。(例如)P7914 [CSP-S 2021] 括号序列

  • 背包

    • [NOIP2006 提高组] 金明的预算方案

      分组背包问题。

      将每个组件和任意个附件看成一组,每种情况相互独立,所以可以看成分组背包。

      先枚举组,在倒序枚举体积,然后枚举一个二进制状态,最后枚举二进制的每一位,算出 vw

    • P1941 [NOIP2014 提高组] 飞扬的小鸟

      模拟 & 背包 & 细节。

      fi,j 表示横坐标为 i ,纵座标为 j 的最少点击次数。

      fi,j=min(fi1,jkX+k,fi1,j+Y)

      时间复杂度: O(nm2)

      优化: 考虑「点击k次」和「点击k1次」之间的联系。最优方案中,点击了k 次到达纵坐标j ,则如果点击 k1 次,会到达纵坐标jX

      类似完全背包的优化思路。

      fi,j=min(fi1,jX+1,fi,jX+1)

      即,「只点击一次」和「从点击若干次到达的将要位置上再点击一次」。

      超过区域的状态应该设为 +,答案简单统计即可。

    • P5020 [NOIP2018 提高组] 货币系统

      排序 & 完全背包。

      思路很好想,从小到大排序。

      大的一定是从小的凑出来的,所以类似筛法,进行 | 运算即可。

      代码:

        sort(a, a + n);
        memset(f, 0, sizeof f);
        f[0] = true;
        int res = n;
          for (int i = 0; i < n; i ++ ){
              if (f[a[i]]) res -- ;
              else
                  for (int j = a[i]; j <= a[n - 1]; j ++ )
                      f[j] |= f[j - a[i]];
          }
        cout<<res<<endl;
      
    • 总结:

      熟记背包模板(01背包,完全背包,多重背包,分组背包)。

      背包优化: 倒序枚举体积优化掉第一维。

  • 状压dp

    • [NOIP2017 提高组] 宝藏

      状压dp。

      fi,j 为 包含状态为 i 的点,且高度为 j 的最小花费。

      状态转移: fi,j=minSifS,j1+j×cost

      cost 为从 iS 的花费。

      Tips:

      枚举真子集:

      for (int i = 1; i < 1 << n; i ++ )
          for (int j = (i - 1) & i; j; j = (j - 1) & i)
      

      枚举子集:

      for (int i = 1; i < 1 << n; i ++ )
          for (int j = i; j; j = (j - 1) & i)
      
    • P2831 [NOIP2016 提高组] 愤怒的小鸟

      抛物线方程为:y=ax2+bx

      只有两个未知数,可以用两个点确定这条抛物线。

      {y1=ax12+bx1y2=ax22+bx2{a=y1x1y2x2x1x2 b=y1x1ax1

      预处理出最多 n2 条合法抛物线,然后用这些抛物线对 点集 进行覆盖即可。

      对于两点构成的抛物线,我们还要处理出他穿过的其他的点。

      然后进行简单的状压 dp 即可。

      时间复杂度: O(n3+n×2n)

    • 总结:

      如果一道题目中 n 很小(n20,可以考虑状压 dp。

      转移一般是从枚举的状态的子集转移过来等。

      枚举子集的时间复杂度 O(3n)

  • 树形dp

    • P5658 [CSP-S2019] 括号树

      • 当树为链时:
        fi 表示以 i 结尾的合法方案数量。

        显然:fi=fj1+1ji 匹配。

      • 当树不为链时:

        我们观察上面的式子,j1 在链上表示 j 的前一个,在树上其实就是表示 j 的父节点。

      一个 dfs 即可完成。

    • P5024 [NOIP2018 提高组] 保卫王国

      树形 dp & 倍增。

      状态定义:

      • fu,0 表示选以 u 为根的子树,且不选择 u 的最小花费。

        fu,1 表示选以 u 为根的子树,且选择 u 的最小花费。

      • gu,0 表示选除了以 u 为根的子树的所有点,且不选择 u 的最小花费。

        gu,1 表示选除了以 u 为根的子树的所有点,且选择 u 的最小花费。

      • wu,i,x,y 表示从 u 开始往上跳 2i 步,设跳到了点 v,且 u 的选择状态为 xx=0 不选,x=1 选), v 的选择状态为 yy=0 不选,y=1 选) ,以 u 为根的子树 减 以 v 为根的子树,剩余部分的最小花费。

      • fau,i 表示从 u 开始往上跳 2i 个点到达的点。

      • depthu 表示 u 的深度。

      状态转移:

      • fu,0=fv,1

        fu,1=min(fv,1,fv,0)

      • gv,0=gu,1+fu,1min(fv,0,fv,1)

        gv,1=min(gv,0,gu,0+fu,0fv,1)

      • wu,0,0,0=

        wu,0,0,1=fv,1min(fu,0,fu,1)

        wu,0,1,1=fv,1min(fu,0,fu,1)

        wu,0,1,0=fu,0fv,1

        wu,i,x,y=min(wu,i1,x,z+wfau,i1,i1,z,y)

      通过简单的计算可以得出,可以画图辅助理解。

      一篇题解 ------ @ 墨染空

      附代码:

       #include <bits/stdc++.h>
       using namespace std;
       typedef long long LL;
       const int N = 100010, M = N * 2, K = 17;
       int n, m;
       int p[N];
       int h[N], e[M], ne[M], idx;
       LL f[N][2], g[N][2], w[N][K][2][2];
       int fa[N][K], depth[N];
       void add(int a, int b)
       {
           e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
       }
       void dfs_f(int u, int father)
       {
           f[u][1] = p[u];
           for (int i = h[u]; ~i; i = ne[i])
           {
               int j = e[i];
               if (j == father) continue;
               dfs_f(j, u);
               f[u][0] += f[j][1];
               f[u][1] += min(f[j][0], f[j][1]);
           }
       }
       void dfs_g(int u, int father)
       {
           for (int i = h[u]; ~i; i = ne[i])
           {
               int j = e[i];
               if (j == father) continue;
       
               g[j][0] = g[u][1] + f[u][1] - min(f[j][0], f[j][1]);
               g[j][1] = min(g[j][0], g[u][0] + f[u][0] - f[j][1]);
       
               dfs_g(j, u);
           }
       }
       void dfs_fa(int u, int father)
       {
           for (int i = h[u]; ~i; i = ne[i])
           {
               int j = e[i];
               if (j == father) continue;
       
               depth[j] = depth[u] + 1;
       
               fa[j][0] = u;
               for (int k = 1; k < K; k ++ )
                   fa[j][k] = fa[fa[j][k - 1]][k - 1];
       
               dfs_fa(j, u);
           }
       }
       void dfs_w(int u, int father)
       {
           for (int i = h[u]; ~i; i = ne[i])
           {
               int j = e[i];
               if (j == father) continue;
       
               w[j][0][0][1] = w[j][0][1][1] = f[u][1] - min(f[j][0], f[j][1]);
               w[j][0][1][0] = f[u][0] - f[j][1];
       
               for (int k = 1; k < K; k ++ )
                   for (int x = 0; x < 2; x ++ )
                       for (int y = 0; y < 2; y ++ )
                           for (int z = 0; z < 2; z ++ )
                               w[j][k][x][y] = min(w[j][k][x][y], w[j][k - 1][x][z] + w[fa[j][k - 1]][k - 1][z][y]);
       
               dfs_w(j, u);
           }
       }
       LL calc(int a, int x, int b, int y)
       {
           if (depth[a] < depth[b]) swap(a, b), swap(x, y);
           if (!x && !y && fa[a][0] == b) return -1;
       
           LL sa[2], sb[2], na[2], nb[2];
           memset(sa, 0x3f, sizeof sa);
           memset(sb, 0x3f, sizeof sb);
           sa[x] = f[a][x], sb[y] = f[b][y];
       
           for (int i = K - 1; i >= 0; i -- )
               if (depth[fa[a][i]] >= depth[b])
               {
                   memset(na, 0x3f, sizeof na);
                   for (int u = 0; u < 2; u ++ )
                       for (int v = 0; v < 2; v ++ )
                           na[v] = min(na[v], sa[u] + w[a][i][u][v]);
                   memcpy(sa, na, sizeof sa);
                   a = fa[a][i];
               }
       
           if (a == b) return sa[y] + g[b][y];
       
           for (int i = K - 1; i >= 0; i -- )
               if (fa[a][i] != fa[b][i])
               {
                   memset(na, 0x3f, sizeof na);
                   memset(nb, 0x3f, sizeof nb);
                   for (int u = 0; u < 2; u ++ )
                       for (int v = 0; v < 2; v ++ )
                       {
                           na[v] = min(na[v], sa[u] + w[a][i][u][v]);
                           nb[v] = min(nb[v], sb[u] + w[b][i][u][v]);
                       }
                   memcpy(sa, na, sizeof sa);
                   memcpy(sb, nb, sizeof sb);
                   a = fa[a][i];
                   b = fa[b][i];
               }
       
           int lca = fa[a][0];
       
           LL res0 = f[lca][0] + g[lca][0] - f[a][1] - f[b][1] + sa[1] + sb[1];
           LL res1 = f[lca][1] + g[lca][1]
               - min(f[a][0], f[a][1]) - min(f[b][0], f[b][1])
               + min(sa[0], sa[1]) + min(sb[0], sb[1]);
       
           return min(res0, res1);
       }
       int main(){
           scanf("%d%d%*s", &n, &m);
           for (int i = 1; i <= n; i ++ ) scanf("%d", &p[i]);
           memset(h, -1, sizeof h);
           for (int i = 0; i < n - 1; i ++ )
           {
               int a, b;
               scanf("%d%d", &a, &b);
               add(a, b), add(b, a);
           }
       
           dfs_f(1, -1);
           dfs_g(1, -1);
           depth[1] = 1;
           dfs_fa(1, -1);
           memset(w, 0x3f, sizeof w);
           dfs_w(1, -1);
           while (m -- ){
               int a, x, b, y;
               scanf("%d%d%d%d", &a, &x, &b, &y);
               printf("%lld\n", calc(a, x, b, y));
           }
           return 0;
       }
      
    • 树形dp 问题,可以考虑先考虑树为一条链的时候,就转换成了线性dp。

      通常情况下状态定义为 fu, 表示以 u 子树的某种状态。

      转移一般有两种,可以用儿子的信息更新父亲,也可以用父亲的信息更新儿子。

  • 倍增优化dp

    • P1081 [NOIP2012 提高组] 开车旅行

      dp & 倍增

      状态定义:

      gai 表示小 A 从城市 i 出发,会走到哪个城市

      gbi 表示小 B 从城市 i 出发,会走到哪个城市

      f0,i,j 表示从城市 i 出发,小 A 先走,走 2j 步会走到哪个城市

      f1,i,j 表示从城市 i 出发,小 B 先走,走 2j 步会走到哪个城市

      da0,i,j 表示从城市 i 出发,小 A 先走,走 2j 步的小 A 走的总距离

      da1,i,j 表示从城市 i 出发,小 B 先走,走 2j 步的小 A 走的总距离

      db0,i,j 表示从城市 i 出发,小 A 先走,走 2j 步的小 B 走的总距离

      db1,i,j 表示从城市 i 出发,小 B 先走,走 2j 步的小 B 走的总距离

      状态计算:

      f0,i,0=gai

      f1,i,0=gbi

      fk,i,1=f1k,fk,i,0,0

      fk,i,j=fk,fk,i,j1,j1  j>1

      da0,i,0=disti,gai  da1,i,0=0

      db1,i,0=disti,gbi  db0,i,0=0

      dak,i,1=dak,i,0+da1k,fk,i,0,0

      dbk,i,1=dbk,i,0+db1k,fk,i,0,0

      dak,i,j=dak,i,j1+dak,fk,i,j1,j1

      dbk,i,j=dbk,i,j1+dbk,fk,i,j1,j1

      通过简单的计算可以得出,可以画图辅助理解。

      附代码:

        #include <iostream>
        #include <cstring>
        #include <algorithm>
        #include <set>
        
        #define x first
        #define y second
        
        using namespace std;
        
        typedef long long LL;
        typedef pair<LL, int> PLI;
        
        const int N = 100010, M = 17;
        const LL INF = 1e12;
        
        int n;
        int h[N];
        int ga[N], gb[N];
        int f[2][N][M];
        LL da[2][N][M], db[2][N][M];
        
        void init_g()
        {
            set<PLI> S;
            S.insert({INF, 0}), S.insert({INF + 1, 0});
            S.insert({-INF, 0}), S.insert({-INF - 1, 0});
        
            PLI cand[4];
        
            for (int i = n; i; i -- )
            {
                PLI t(h[i], i);
                auto j = S.upper_bound(t);
                j ++ ;
        
                for (int k = 0; k < 4; k ++ )
                {
                    cand[k] = *j;
                    j -- ;
                }
        
                LL d1 = INF, d2 = INF;
                int p1 = 0, p2 = 0;
                for (int k = 3; k >= 0; k -- )
                {
                    LL d = abs(h[i] - cand[k].x);
                    if (d < d1)
                    {
                        d2 = d1, d1 = d;
                        p2 = p1, p1 = cand[k].y;
                    }
                    else if (d < d2)
                    {
                        d2 = d;
                        p2 = cand[k].y;
                    }
                }
                ga[i] = p2, gb[i] = p1;
                S.insert(t);
            }
        }
        
        void init_f()
        {
            for (int i = 1; i <= n; i ++ )
            {
                f[0][i][0] = ga[i];
                f[1][i][0] = gb[i];
            }
            for (int j = 1; j < M; j ++ )
                for (int i = 1; i <= n; i ++ )
                    for (int k = 0; k < 2; k ++ )
                    {
                        if (j == 1)
                            f[k][i][j] = f[1 - k][f[k][i][0]][0];
                        else
                            f[k][i][j] = f[k][f[k][i][j - 1]][j - 1];
                    }
        }
        
        int get_dist(int a, int b)
        {
            return abs(h[a] - h[b]);
        }
        
        void init_d()
        {
            for (int i = 1; i <= n; i ++ )
            {
                da[0][i][0] = get_dist(i, ga[i]);
                db[1][i][0] = get_dist(i, gb[i]);
            }
            for (int j = 1; j < M; j ++ )
                for (int i = 1; i <= n; i ++ )
                    for (int k = 0; k < 2; k ++ )
                    {
                        if (j == 1)
                        {
                            da[k][i][j] = da[k][i][j - 1] + da[1 - k][f[k][i][j - 1]][j - 1];
                            db[k][i][j] = db[k][i][j - 1] + db[1 - k][f[k][i][j - 1]][j - 1];
                        }
                        else
                        {
                            da[k][i][j] = da[k][i][j - 1] + da[k][f[k][i][j - 1]][j - 1];
                            db[k][i][j] = db[k][i][j - 1] + db[k][f[k][i][j - 1]][j - 1];
                        }
                    }
        }
        
        void calc(int s, int x, int& la, int& lb)
        {
            la = lb = 0;
            for (int i = M - 1; i >= 0; i -- )
                if (f[0][s][i] && la + lb + da[0][s][i] + db[0][s][i] <= x)
                {
                    la += da[0][s][i], lb += db[0][s][i];
                    s = f[0][s][i];
                }
        }
        
        int main()
        {
            scanf("%d", &n);
            for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
        
            init_g();
            init_f();
            init_d();
        
            int x;
            scanf("%d", &x);
            int res = 0, max_h = 0;
            double min_ratio = INF;
            for (int i = 1; i <= n; i ++ )
            {
                int la, lb;
                calc(i, x, la, lb);
                double ratio = lb ? (double)la / lb : INF;
                if (ratio < min_ratio || ratio == min_ratio && h[i] > max_h)
                {
                    min_ratio = ratio;
                    max_h = h[i];
                    res = i;
                }
            }
            printf("%d\n", res);
        
            int m;
            scanf("%d", &m);
            while (m -- )
            {
                int s, x, la, lb;
                scanf("%d%d", &s, &x);
                calc(s, x, la, lb);
                printf("%d %d\n", la, lb);
            }
        
            return 0;
        }
      
    • 总结:

      倍增优化dp,可以先考虑朴素dp,结合 n 的范围可以确定是否可以倍增优化。这类题目难度一般比较大

  • dp 杂题:

    • P1850 [NOIP2016 提高组] 换教室

      dp & 期望 & Floyd

      状态表示:

      fi,j,0 表示前 i 个课程,申请换了 j 次,且最后一次没申请换的最小期望长度。

      fi,j,1表示前 i 个课程,申请换了 j 次,且最后一次申请交换的最小期望长度。

      fi,j,0 在如下两种情况中取最小值即可:

      • i1 个课程没申请交换,最小期望是 fi1,j,0+dai1,ai

      • i1 个课程申请交换,最小期望是 fi1,j,1+dai1,ai×(1pi1)+dbi1,ai×pi1

      fi,j,1 推导类似。

      时间复杂度: O(V3+nm)

    • P1514 [NOIP2010 提高组] 引水入城

      记忆化搜索 & 贪心(区间覆盖)

      第一问直接 DFS + 记忆化即可。

      可以证明每个点覆盖到的一定是一段区间。

      如果中间有空隙,那么一定会有另一条水流流向那个点,而原来的水流也可以经过 这两条水流的交点 到达哪个空隙,矛盾。

      简单的搜索加上区间覆盖模板即可通过。

    • 总结:

      dp 问题很复杂,难点在于状态定义。

      一般有一些固定的套路,如线性 dp,区间 dp 等。

      状态转移就是 集合划分,考虑这个集合可以划分成哪几种不重不漏的集合,然后进行转移。

      初值和边界很重要,一般按照状态定义来写。

      总之 dp问题难想 & 细节多,一定要仔细想 & 仔细检查。

      一些dp模板也很重要(背包,数位 dp 等)。

贪心:

  • P1090 [NOIP2004 提高组] 合并果子

    贪心 & 堆优化

    很明显每次取出最小的两个合并是最优的。

    可以用堆优化。

    时间复杂度 O(nlogn)

  • P1080 [NOIP2012 提高组] 国王游戏

    贪心 & 高精度

    大胆猜结论:按照 ai×bi 从小到大排序为最优解。

    证明:

    假设将两个人的位置互换,考虑他们在交换前和交换后所获得的奖励是多少:

    • 交换前:

      • i 个人 j=0i1AjBi

      • i+1 个人 j=0iAjBi+1

    • 交换后:

      • i 个人 j=0i1AjBi+1
      • i+1 个人 Ai+1j=0i1AjBi

    将每个数除以 j=0i1Aj,然后乘 BiBi+1,得到:

    • 交换前 Bi+1 AiBi

      • i 个人 Bi+1

      • i+1 个人 AiBi

    • 交换后 Bi Ai+1Bi+1

      • i 个人 Bi

      • i+1 个人 Ai+1Bi+1

    由于 Ai>0,所以 BiAiBi,并且 AiBi>Ai+1Bi+1,所以 max(Bi,Ai+1Bi+1)AiBimax(Bi+1,AiBi),

    所以交换后两个数的最大值不大于交换前两个数的最大值。

    证毕!

  • P5019 [NOIP2018 提高组] 铺设道路

    贪心

    如果 ai>ai1 那么答案加上 aiai1

    因为如果 aiai1 那么在填 i1 的时候一定能把 i 一块填上。否则给 ai 填上 ai1 ,枚举到 i 的时候只需要再填 aiai1 即可。

  • P1969 [NOIP2013 提高组] 积木大赛

    P5019 [NOIP2018 提高组] 铺设道路 一样,不多赘述。

  • P1970 [NOIP2013 提高组] 花匠

    贪心。

    转化题意,就是让我们求出最长的一个波动序列的长度。

    序列中的每一个极值点(波峰,波谷)都是可以选择的,于是我们统计出所有的极值点即可(可以证明不存在比它更优的结果)。

  • P1315 [NOIP2011 提高组] 观光公交

    贪心 & 递推

    预处理出每个站台的最早发车时间 lasti,每个站台下车的人数 sumi

    接下来求出车最早到达每个站台的时间 tmi

    tmi=max(tmi1,lasti1)+di1, 其中 di1 是从第i 1 个站台走到第 i 个站台的时间。

    那么每个乘客的旅行时间就是 tmbiti,其中 bi 是乘客的终点站,ti 是乘客到达起点的时间。

    考虑氮气加速用在哪里

    可以发现如下几个性质:

    • 每次加速一段之后,可能会影响接下来一段连续的站点。因此在区间内部,加速最左端的站点一定是最优的。

    • 不同红色区间之前完全独立,加速其中某个区间时,对其余区间没有任何影响。

    • 加速某个区间左端点之后,该区间可能会分裂成两个子区间,这两个子区间的加速效果小于等于原区间的加速效果。

    每次选择当前节约时间最多的一段即可。

    时间复杂度 O(n2)

  • P7078 [CSP-S2020] 贪吃蛇

    太难了不想写了,如下:

    题解 ------ @ OMG_wc

  • P5665 [CSP-S2019] 划分

    太难了不想写了,如下:

    题解 ------ @ 1saunoya

  • 总结:

    贪心题目可以先考虑 dp,如果dp比较好做就选择使用dp,如果dp不好做并且贪心策略大致是正确的时候可以选择贪心。

    贪心题就要大胆才猜结论,写个程序跑一下样例过了一般就对了。

    我们 OIer 做题不需要证明,跑一下即可。

搜索:

  • DFS

  • BFS

  • 剪枝

数学:

  • 数论:

  • 组合数学

图论:

  • 拓扑排序

  • LCA

  • 最短路

  • 二分图

数据结构:

  • 并查集

  • 线段树

  • 单调队列

基础算法:

posted @   Star_F  阅读(512)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示