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

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

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

如需转载请联系我~

作者个人资料


DP:

  • 线性dp

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

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

    • P1052 [NOIP2005 提高组] 过河

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

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

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

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

    • P1006 [NOIP2008 提高组] 传纸条

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

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

    • P1541 [NOIP2010 提高组] 乌龟棋

      记录 \(dp_{A,B,C,D}\) 表示用了 \(A\)\(1\) 号卡片, \(B\)\(2\) 号卡片, \(C\)\(3\) 号卡片, \(D\)\(4\) 号卡片。

      暴力转移即可 \(F_{A,B,C,D}=\max (F_{A-1,B,C,D},F_{A,B-1,C,D},F_{A,B,C-1,D},F_{A,B,C,D-1})+cost_{A+2B+3C+4D}\)

    • P2679 [NOIP2015 提高组] 子串

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

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

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

      状态计算:\(f_{i,j,k}\)表示的所有方案分成两大类:

      • 不用 \(S_i\),则方案数是 \(f_{i-1,j,k}\)
      • 使用 \(S_i\),则方案数是 $ \sum f_{i-t,j-t,k-1}$,满足 \(S_{i-t+1}=T_{i-t+1}\)\(t\) 从小到大枚举。

      时间复杂度是 $ \mathcal{O} (nm2k)$。

      我们发先 \(f_{i,j,k}\) 的第二项和 \(f_{i-1,j-1,k}\) 很像,可以考虑维护一个 \(tmp_{i,j,k}\)

      • \(S_i=T_j\) , \(tmp_{i,j,k}=tmp_{i-1,j-1,k}+f_{i-1,j-1,k-1}\)
      • \(S_i \neq T_j\)\(tmp_{i,j,k}=0\)

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

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

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

      求解所有方案数

      \(f_{i,j}\) 表示前 \(i\) 种烹饪方式,做了 \(j\) 道菜的方案数。

      • 状态转移:
        \(i\) 种烹饪方式不做 \(f_{i,j}+= f_{i - 1,j}\)

      • \(i\) 种烹饪方法做 \(1\) 道主要食材是 \(k\) 的菜:\(f_{i,j} += f_{i-1,j-1} * a_{i, k}\)

      所有方案数量 $ A = \sum\limits_{j = 1}^{n}f_{n,j}$ 。

      求解不合法方案:

      \(dp_{i,j}\) 表示前 \(i\) 中烹饪方法,越界食材数 $ - $ 其他食材数 为 \(j\) 的方案数。

      状态转移:

      • \(i\) 种烹饪方法不选:\(dp_{i,j} += dp_{i-1,j}\)

      • 选越界食材 \(c\)\(dp_{i,j} += dp_{i-1,j-1} * a_{i, c}\)

      • 选其他食材 \(dp_{i,j} += dp_{i-1,j+1} * (s_i - a_{i, c})\)

      所有方案数量: $ B = \sum dp_{n,j} (j > 0)$ 。

      \(A-B\) 即可。

      时间复杂度 \(\mathcal{O}(n ^ 2m)\)

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

    • 总结

      对于线性 dp的问题,一般状态定义\(f_{前\ i\ 个,满足 \ ....\ 状态}\),状态可能很多,所以可能有很多维。

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

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

  • 区间dp

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

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

      \(f_{i,j}\) 为 区间 \([i\cdots j]\) 的最大价值。

      转移:$f_{i,j} = \max\limits_{k=i}^{j} f_{i,k-1} \times f_{k+1,j} + w_k $

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

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

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

      \(f_{i,j}\) 为 区间 \([i\cdots j]\) 的最大价值。

      转移:$f_{i,j} = \max\limits_{k=i}^{j} f_{i,k} + f_{k,j} + w_i \times w_j \times w_k $。

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

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

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

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

      \(f_{i,j}\) 表示将 \([i \cdots j]\) 这段数取完的最大值。

      转移

      • 取左端点:\(f_{i+1,j} + w_i \times 2^{m-j+i}\)

      • 取右端点:\(f_{i,j+1} + w_j \times 2^{m-j+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 & 分类讨论

      状态定义:

      \(dp_{i,j}\) 表示从 \(i\)\(j\) 合法序列数量。

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

      将两维的dp扩充为三维,第三维表示不同的形态种类,dp状态就变成了 \(dp_{i,j,[0,5]}\)

      • \(dp_{i,j,0}\): 形态如***...*的括号序列(即全部是*)。

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

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

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

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

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

      \(\forall i\) 满足 \(1\le i \le n\),有 \(dp_{i,i-1,0}=1\)

      状态转移:

      • \(dp_{l,r,0}\)(直接特判)

      • \(dp_{l,r,1}=(dp_{l+1,r-1,0}+dp_{l+1,r-1,2}+dp_{l+1,r-1,3}+dp_{l+1,r-1,4})*compare(l,r)\)

        1. \(compare(i,j)\) 表示第 \(i\) 位与第 \(j\) 位能否配对成括号,能则为 \(1\),否则为 \(0\)
        2. 加括号时,里面可以是全*,可以是有一边是*,也可以是两边都不是*,唯独不能两边都是*且中间有括号序列。
      • \(dp_{l,r,2}=\sum\limits_{i=l}^{r-1} dp_{l,i,3}\times dp_{i+1,r,0}\)

        1. 左边以括号序列开头且以括号序列结尾的是第3种,右边接一串*,是第0种。
      • \(dp_{l,r,3}=\sum\limits_{i=l}^{r-1} (dp_{l,i,2}+dp_{l,i,3})\times dp_{i+1,r,1}+dp_{l,r,1}\)

        1. 左边以括号序列开头,结尾随便,符合的有第2和第3种,右边接一个括号序列,是第1种。
        2. 记得加上直接一个括号序列的。
      • \(dp_{l,r,4}=\sum\limits_{i=l}^{r-1} (dp_{l,i,4}+dp_{l,i,5})\times dp_{i+1,r,1}\)

        1. 左边以*开头,结尾随便,符合的有第4和第5种,右边接一个括号序列,是第1种。
      • \(dp_{l,r,5}=\sum\limits_{i=l}^{r-1} dp_{l,i,4}\times dp_{i+1,r,0}+dp_{l,r,0}\)

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

      答案: \(dp_{1,n,3}\)

      时间复杂度: \(\mathcal{O}(n^3)\)

    • 总结:

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

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

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

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

  • 背包

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

      分组背包问题。

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

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

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

      模拟 & 背包 & 细节。

      \(f_{i,j}\) 表示横坐标为 \(i\) ,纵座标为 \(j\) 的最少点击次数。

      $ f_{i,j}=\min(f_{i-1,j-kX}+k,f_{i-1,j+Y})$

      时间复杂度: \(\mathcal{O}(nm^2)\)

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

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

      \(f_{i,j}=min(f_{i-1,j-X}+1,f_{i,j-X}+1)\)

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

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

    • 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。

      \(f_{i,j}\) 为 包含状态为 \(i\) 的点,且高度为 \(j\) 的最小花费。

      状态转移: \(f_{i,j} = \min\limits _{S\subsetneqq i} f_{S,j-1} + j \times cost\)

      \(cost\) 为从 \(i\)\(S\) 的花费。

      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=ax^2 + bx\)

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

      \[{\begin{cases} y_1 = ax_1^2 + bx_1 \\\\ y_2 = ax_2^2 + bx_2\end{cases}} \quad \Rightarrow \quad {\begin{cases} a = \dfrac{\dfrac{y_1}{x_1} - \dfrac{y_2}{x_2}}{x_1 - x_2}\\\\\ b = \dfrac{y_1}{x_1} - ax_1\end{cases}} \]

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

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

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

      时间复杂度: \(\mathcal{O(n^3+n \times 2^n)}\)

    • 总结:

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

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

      枚举子集的时间复杂度 $\mathcal{O(3^n)} $ 。

  • 树形dp

    • P5658 [CSP-S2019] 括号树

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

        显然:\(f_i = f_{j-1} + 1\)\(j\)\(i\) 匹配。

      • 当树不为链时:

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

      一个 dfs 即可完成。

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

      树形 dp & 倍增。

      状态定义:

      • \(f_{u,0}\) 表示选以 \(u\) 为根的子树,且不选择 \(u\) 的最小花费。

        \(f_{u,1}\) 表示选以 \(u\) 为根的子树,且选择 \(u\) 的最小花费。

      • \(g_{u,0}\) 表示选除了以 \(u\) 为根的子树的所有点,且不选择 \(u\) 的最小花费。

        \(g_{u,1}\) 表示选除了以 \(u\) 为根的子树的所有点,且选择 \(u\) 的最小花费。

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

      • \(fa_{u,i}\) 表示从 \(u\) 开始往上跳 \(2^i\) 个点到达的点。

      • \(depth_{u}\) 表示 \(u\) 的深度。

      状态转移:

      • \(f_{u,0} = \sum f_{v,1}\)

        \(f_{u,1} = \sum \min (f_{v,1},f_{v,0})\)

      • \(g_{v,0} = g_{u,1} + f_{u,1} - \min (f_{v,0},f_{v,1})\)

        \(g_{v,1} = \min (g_{v,0},g_{u,0} + f_{u,0} - f_{v,1})\)

      • \(w_{u,0,0,0} = \infty\)

        \(w_{u,0,0,1} = f_{v,1} - \min (f_{u,0},f_{u,1})\)

        \(w_{u,0,1,1} = f_{v,1} - \min (f_{u,0},f_{u,1})\)

        $w_{u,0,1,0} = f_{u,0} - f_{v,1} $

        \(w_{u,i,x,y} = \min (w_{u,i-1,x,z} + w_{{fa_{u,i-1}},i-1,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。

      通常情况下状态定义为 \(f_{u,状态 \cdots }\) 表示以 \(u\) 子树的某种状态。

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

  • 倍增优化dp

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

      dp & 倍增

      状态定义:

      \(ga_i\) 表示小 A 从城市 \(i\) 出发,会走到哪个城市

      \(gb_i\) 表示小 B 从城市 \(i\) 出发,会走到哪个城市

      \(f_{0,i,j}\) 表示从城市 \(i\) 出发,小 A 先走,走 \(2^j\) 步会走到哪个城市

      \(f_{1,i,j}\) 表示从城市 \(i\) 出发,小 B 先走,走 \(2^j\) 步会走到哪个城市

      \(da_{0,i,j}\) 表示从城市 \(i\) 出发,小 A 先走,走 \(2^j\) 步的小 A 走的总距离

      \(da_{1,i,j}\) 表示从城市 \(i\) 出发,小 B 先走,走 \(2^j\) 步的小 A 走的总距离

      \(db_{0,i,j}\) 表示从城市 \(i\) 出发,小 A 先走,走 \(2^j\) 步的小 B 走的总距离

      \(db_{1,i,j}\) 表示从城市 \(i\) 出发,小 B 先走,走 \(2^j\) 步的小 B 走的总距离

      状态计算:

      \(f_{0,i,0} = ga_i\)

      \(f_{1,i,0} = gb_i\)

      \(f_{k,i,1} = f_{1-k,f_{k,i,0},0}\)

      \(f_{k,i,j} = f_{k,f_{k,i,j-1},j-1} \ \ j > 1\)

      \(da_{0,i,0} = dist_{i,ga_i} \ \ da_{1,i,0} = 0\)

      \(db_{1,i,0} = dist_{i,gb_i} \ \ db_{0,i,0} = 0\)

      \(da_{k,i,1} = da_{k,i,0} + da_{1-k,f_{k,i,0},0}\)

      \(db_{k,i,1} = db_{k,i,0} + db_{1-k,f_{k,i,0},0}\)

      \(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}\)

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

      附代码:

        #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

      状态表示:

      \(f_{i,j,0}\) 表示前 \(i\) 个课程,申请换了 \(j\) 次,且最后一次没申请换的最小期望长度。

      \(f_{i,j,1}\)表示前 \(i\) 个课程,申请换了 \(j\) 次,且最后一次申请交换的最小期望长度。

      \(f_{i,j,0}\) 在如下两种情况中取最小值即可:

      • \(i - 1\) 个课程没申请交换,最小期望是 \(f_{i-1,j,0} + d_{a_{i-1},a_i}\)

      • \(i - 1\) 个课程申请交换,最小期望是 \(f_{i-1,j,1} + d_{a_{i-1},a_i} \times (1 - p_{i - 1}) + d_{b_{i-1},a_i} \times p_{i-1}\)

      \(f_{i,j,1}\) 推导类似。

      时间复杂度: \(\mathcal{O}(V^3 + nm)\)

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

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

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

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

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

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

    • 总结:

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

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

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

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

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

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

贪心:

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

    贪心 & 堆优化

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

    可以用堆优化。

    时间复杂度 \(\mathcal{O} (n \log n)\)

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

    贪心 & 高精度

    大胆猜结论:按照 \(a_i \times b_i\) 从小到大排序为最优解。

    证明:

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

    • 交换前:

      • \(i\) 个人 \(\frac{\prod_{j=0}^{i-1}A_j}{B_i}\)

      • \(i + 1\) 个人 \(\frac{\prod_{j=0}^{i}A_j}{B_{i + 1}}\)

    • 交换后:

      • \(i\) 个人 \(\frac{\prod_{j=0}^{i-1}A_j}{B_{i + 1}}\)
      • \(i + 1\) 个人 \(\frac{A_{i + 1} * \prod_{j=0}^{i - 1}A_j}{B_i}\)

    将每个数除以 \(\prod_{j=0}^{i-1}A_j\),然后乘 \(B_i * B_{i + 1}\),得到:

    • 交换前 \(B_{i + 1}\) \(A_i * B_i\)

      • \(i\) 个人 \(B_{i + 1}\)

      • \(i + 1\) 个人 \(A_i * B_i\)

    • 交换后 \(B_i\) \(A_{i + 1} * B_{i + 1}\)

      • \(i\) 个人 \(B_i\)

      • \(i + 1\) 个人 \(A_{i + 1} * B_{i + 1}\)

    由于 \(A_i > 0\),所以 \(B_i \le A_i * B_i\),并且 \(A_i * B_i > A_{i+1} * B_{i+1}\),所以 \(\max(B_i, A_{i + 1} * B_{i + 1}) \le A_i * B_i \leq max(B_{i + 1}, A_i * B_i)\),

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

    证毕!

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

    贪心

    如果 $a_i > a_{i-1} $ 那么答案加上 \(a_i - a_{i-1}\)

    因为如果 $a_i \leq a_{i-1} $ 那么在填 \(i-1\) 的时候一定能把 \(i\) 一块填上。否则给 \(a_i\) 填上 \(a_{i-1}\) ,枚举到 \(i\) 的时候只需要再填 \(a_i -a_{i-1}\) 即可。

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

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

  • P1970 [NOIP2013 提高组] 花匠

    贪心。

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

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

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

    贪心 & 递推

    预处理出每个站台的最早发车时间 \(last_i\),每个站台下车的人数 \(sum_i\)

    接下来求出车最早到达每个站台的时间 \(tm_i\)

    \(tm_i = \max(tm_{i-1}, last_{i-1}) + d_{i - 1}\), 其中 \(d_{i - 1}\) 是从第i $ - 1$ 个站台走到第 \(i\) 个站台的时间。

    那么每个乘客的旅行时间就是 \(tm_{b_i} - t_i\),其中 \(b_i\) 是乘客的终点站,\(t_i\) 是乘客到达起点的时间。

    考虑氮气加速用在哪里

    可以发现如下几个性质:

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

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

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

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

    时间复杂度 \(\mathcal{O(n^2)}\)

  • P7078 [CSP-S2020] 贪吃蛇

    太难了不想写了,如下:

    题解 ------ @ OMG_wc

  • P5665 [CSP-S2019] 划分

    太难了不想写了,如下:

    题解 ------ @ 1saunoya

  • 总结:

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

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

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

搜索:

  • DFS

  • BFS

  • 剪枝

数学:

  • 数论:

  • 组合数学

图论:

  • 拓扑排序

  • LCA

  • 最短路

  • 二分图

数据结构:

  • 并查集

  • 线段树

  • 单调队列

基础算法:

posted @ 2024-10-02 10:50  Star_F  阅读(996)  评论(0)    收藏  举报