2 条题解

  • 1
    @ 2024-7-28 16:24:19

    从点 nn 开始,只有两种跳法:跳到 n1n-1 处,或者跳到 nn 的因子 dd 处. 跳到 n1n-1 处后等同于从 n1n-1 处开始,但跳到 dd 和从 dd 开始有一定区别,因为跳完后还可以往前跳(只要不超过 n1n-1 即可).

    (上图黑线表示第一步、蓝线表示第二步,直线表示从 ii 跳到 i±1i\pm1 的类型,曲线从 ii 跳到 ii 的因子的类型.)

    我们用 fi,jf_{i,j} 表示从 ii 出发到 11,且经过的点在 [1,j][1,j] 范围内的方案数,我们要求的答案即为 fn,nf_{n,n}. 根据前面的分析,可以得到:

    fi,i=fi1,i1+difd,i1f_{i,i} = f_{i-1,i-1} + \sum_{d\mid i}f_{d,i-1}

    那么对于 j>ij>ifi,jf_{i,j} 怎么求呢?首先根据定义 fi,j1f_{i,j-1} 的方案对 fi,jf_{i,j} 有贡献.

    如果我们要到达 jj 点,必须是刚开始就从 ii 向前跳到 jj,到达 jj 点后,不能再到 j1j-1 或者 j+1j+1,只能跳到 jj 的因子处离开,且这个因子必须小于 ii.

    可以得到:

    fi,j=fi,j1+djfd,i1f_{i,j}=f_{i,j-1} + \sum_{d|j}f_{d,i-1}

    观察到我们推的两个式子形式相似,可以放在一个循环中,动态规划的边界即为 f1,i=1f_{1,i} = 1.

    对于每个数,我们可以提前预处理因子,算法的时间复杂度为 nn+n2d(n)n\sqrt n+n^2\cdot d(n),优化一下常数,把对 kk 取模改成减去 kk 即可通过.

    代码:

    #include <iostream>
    #include <vector>
    const int MAXN = 5005;
    using namespace std;
    int n;
    long long f[MAXN][MAXN], mod;
    vector<int> d[MAXN];
    int main() {
        cin >> n >> mod;
        for (int i = 1; i <= n; ++i) f[1][i] = 1;
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j * j <= i; ++j) {
                if (i % j == 0) {
                    d[i].push_back(j);
                    if (i / j != j) d[i].push_back(i / j);
                }
            }
        }
        for (int i = 2; i <= n; ++i) {
            f[i][i] = f[i - 1][i - 1];
            for (int j = i; j <= n; ++j) {
                f[i][j] += f[i][j - 1];
                for (int k : d[j]) {
                    if (k < i) {
                        f[i][j] += f[k][i - 1];
                        if (f[i][j] > mod) f[i][j] -= mod;
                    }
                }
            }
        }
        cout << f[n][n] % mod << endl;
        return 0;
    }
    
    • 0
      @ 2025-3-23 16:43:52

      666 这场比赛怎么全是 DP。

      题意

      连续 nn 个下标 1,2,3,...,n1,2,3,...,n,最开始你在下标 nn,每次可以从 ii 走到 i1i-1i+1i+1,或者走到 ii 的约数下标(这是两种不同的走法),每个下标只能走一次,问你有多少种从 nn 走到 11的走法(不能走出 11nn 的范围)。

      思考过程

      如果只有向一边走的情况,那么这个题很容易做,但是这里多出来一种向回走的情况,这就不太好考虑了。

      但是我们可以发现,向回走一定是只能一格一格的走,并且一定存在于原来某次跳约数的中间未被访问的部分。

      比如下图中从 88 跳到 22(跳约数),这样的话,3377 的下标都没有被跳过,因此完全可以考虑这个时候从 22 往回一格一格跳,比如考虑跳到 55,那么此时所有能够跳到 88 的方案,通过 22 一定能够跳到 55,所以此时可以让 55 累加上跳到 88 的方案,注意不是加 22 的,22 中包含有其他的方案,与我们现在讨论的不一致。

      但是直接加上 88 的就好了吗?实际上还存在一类问题。

      绿色和红色是曾今走到 88 的一种决策,这一决策经过了 55,而如今我们在对 55 进行决策时,却没有考虑到 55 已经被先前的某些方案所覆盖(已经被走过了),所以我们必须记录一下,点 ii 是从前面哪个点往后走到的,比如这里 88 是从 55 走到的,所以后来我们在考虑蓝色路径时(对 55 的决策),只能让 55 累加上从 6,76,7 走到 88(这个东西就是个后缀和嘛),或者从 88 后面来到 88 的情况的方案数。

      所以接下来思路就很明确了:

      定义状态 fi,jf_{i,j} 表示从 j,j+1,j+2,...,ij,j+1,j+2,...,i 向后走走到的 ii 的方案数,这里我为了好统计,将 fi,if_{i,i} 的状态特殊定义为从 ii 后面走到 ii 的方案数。

      对于向前一格的情况:fi,i=fi,i+fi+1,i+1f_{i,i}=f_{i,i}+f_{i+1,i+1}

      对于从 ll 跳约数来到 ii 的情况:fi,i=fi,i+fl,i+1f_{i,i}=f_{i,i}+f_{l,i+1}iill 的约数)

      接下来着重看一下往回跳的情况

      下面是 i=2i=2 的情况,我们考虑 jj 通过约数的跳法来到 ii,此时我们在 i+1i+1j1j-1 中枚举 kk,此时将 kk 累加上 jjk+1,k+2,k+3,...,jk+1,k+2,k+3,...,j 跳过来的方案数:对于 i+1kj1i+1\le k\le j-1fk,i=fk,i+fj,k+1f_{k,i}=f_{k,i}+f_{j,k+1}(从后面来到 jj 的方案数根据定义统计在 fj,jf_{j,j} 中,被正常累计在 kk 上)。

      我们是在对第 fj,if_{j,i} 中的第二维进行后缀和,第二维就是下图中的 ii(对于每次枚举的 ii,只需要修改 fi,if_{i,i}fk,if_{k,i}),所以对于每个 ii 计算完后,再刷一遍 ii 的后缀和就好了。具体的,每次关于 ii 的计算结束后,枚举 1kn1\le k\le n,令 fk,i=fk,i+fk,i+1f_{k,i}=f_{k,i}+f_{k,i+1}


      时间复杂度

      第一层循环一个 ii,第二层循环 ii 的倍数 jj(不包括 ii),第三层循环 iijj 的所有数

      第一层可以看成 n2\frac{n}{2},因为再往上就不会有 ii 的倍数了(因为倍数都超过 nn )。

      接下来第二层和第三层是一个等差数列求和,公差为 ii。对于一个 ii,需要计算 $[i,2\times i],[i,3\times i],[i,4\times i],...,[i,n]$ 这些区间(每个区间长度比前一个多 ii,共有 nii\frac{n-i}{i} 个区间),计算次数就是 $(2\times i-i)+(3\times i-i)+(4\times i-i)+...+(n-i)$,这里直接把 nin-i 看成 nn 就好。

      $i+2\times i+3\times i+...+n=\frac{n-i}{i}\times \frac{(n+i)}{2}$,nii\frac{n-i}{i} 是项数。

      nin-i 估大成 nn,把 n+in+i 估大成 2×n2\times n

      得到 $\frac{n}{i}\times\frac{2\times n}{2}=\frac{2\times n^2}{2\times i}=\frac{n^2}{i}$,再把 ii 带入得到 $\displaystyle\sum_{i=1}^{\frac{n}{2}}\frac{n^2}{i}=n\times \displaystyle\sum_{i=1}^{\frac{n}{2}} \frac{n}{i}=n^2\times\ln(n)$,ok拿下。

      #include<bits/stdc++.h>
      using namespace std ;
      const int Max = 5e3 + 10 ; 
      vector < int > Start[Max] , Prinm[Max] ; 
      int g[Max][Max] ; 
      int f[Max][Max] ;
      int n , p ;
      int main( ) {
      	scanf("%d%d" , &n , &p ) ; 
      	// 边界情况特殊处理 
      	f[n + 1][n + 1] = 1 ; 
      	for(int i = n ; i >= 1 ; i -- ) {
      		// i 从 i+1 走来
      		// 所以对 f[i][i] 修改即可 
      		f[i][i] = ( f[i][i] + f[i + 1][i + 1] ) % p ;  
      		
      		for(int l = 2 * i ; l <= n ; l += i ) {
      			// i 从 l 跳来,这里注意只有 l 是从 i+1,i+2,...,l 跳过来的方案需要被累计 
      			f[i][i] = ( f[i][i] + f[l][i + 1] ) % p ; 
      			
      			for(int k = i + 1 ; k <= l - 1 ; k ++ ) {
      				// l 从 k+1,k+2,...,l 跳过来
      				// k 从 i 跳过来 
      				f[k][i] = ( f[k][i] + f[l][k + 1] ) % p ; 
      				
      			}
      		}
      		// 每次 i 计算完,救刷新一下后缀和 
      		for(int l = 1 ; l <= n ; l ++ ) f[l][i] = ( f[l][i] + f[l][i + 1] ) % p ; 
      	}
      	// 答案就是从 1 后面跳过来 
      	printf("%d\n" , f[1][1] ) ;
      	return false ; 
      }
      
      • 1

      信息

      ID
      19
      时间
      2000ms
      内存
      512MiB
      难度
      6
      标签
      递交数
      212
      已通过
      45
      上传者