MT2147 纸带
难度:钻石 时间限制:2秒 占用内存:128M
题目描述
小码哥有一张 1 × n 1\times n 1×n 的纸带,由 n n n 个格子组成。初始有一个点在 n n n 号格子(即左数第 n n n 个)中。
假设现在这个点在 x ( x > 1 ) x(x>1) x(x>1) 号格子,每次小码哥可以对这个点进行如下操作中的一种:
- 减法。选择一个 [ 1 , x − 1 ] [1,x-1] [1,x−1] 中的正整数 y y y,将点移动到 x − y x-y x−y 号格子中。
- 除法。选择一个 [ 2 , x ] [2,x] [2,x] 中的正整数 y y y,将点移动到 ⌊ x y ⌋ \left\lfloor\frac{x}{y}\right\rfloor ⌊yx⌋ 号格子中。
当点在 1 号格子中时无法移动,操作结束。
求将点从 n n n 号格子移到 1 号格子的方案数,答案对给定的模数取模。
两个方案不同当且仅当有一步选择的操作或选择的数不同。格式
输入格式:一行两个数,纸带长度 n n n 和模数 m m m;
输出格式:一个数,表示答案对 m m m 取模的结果。样例 1
输入:3 998244353
输出:5
备注
其中: 2 ≤ n ≤ 4 e 6 , 1 e 8 < m < 1 e 9 , m 2\le n\le4e6,\ 1e8
2≤n≤4e6, 1e8<m<1e9, m 是质数。
相关知识点:
动态规划
、前缀和
这道题需要对整数 n n n 进行若干次自减(减数的取值范围为 [ 1 , n − 1 ] \left[1,\ n-1\right] [1, n−1])或整除(除数的取值范围为 [ 2 , n ] \left[2,\ n\right] [2, n],对除法结果采取向下取整)运算,问将 n n n 降至 1 有多少种不同的运算方式?
对于这个问题,可设 d p [ i ] dp[i] dp[i] 为 “数 i i i 降至 1 有 d p [ i ] dp[i] dp[i] 种不同的运算方式” ,则初始情况下有 d p [ 1 ] = 1 dp[1]=1 dp[1]=1(表示走到了数 1,这是一种运算方式,且是唯一的),最终待求目标即为 d p [ n ] dp[n] dp[n]。下面讨论该模型的转移方程。
对于任意数 i i i,若选择自减运算,则其可选择的减数有 { 1 , 2 , ⋯ , i − 1 } \left\{1,\ 2,\cdots,i-1\right\} {1, 2,⋯,i−1} (共 i − 1 i-1 i−1 种)。每次减掉该数后(假设选择的减数为 k k k),其都将来到 i − k i-k i−k ,而从 i − k i-k i−k 到 1 的运算方式在此之前已经得到(为 d p [ i − k ] dp[i-k] dp[i−k])。所以选择自减运算的更新公式为:
for(int j=1; j
若选择整除运算,则其可选择的除数有 { 2 , 3 , ⋯ , i } \left\{2,\ 3,\cdots,i\right\} {2, 3,⋯,i}(共 i − 1 i-1 i−1 种)。每次除掉该数后(假设选择的除数为 k k k),其都将来到 ⌊ i k ⌋ \left\lfloor\frac{i}{k}\right\rfloor ⌊ki⌋,而从 ⌊ i k ⌋ \left\lfloor\frac{i}{k}\right\rfloor ⌊ki⌋ 到 1 的运算方式在此之前也已经得到(为 d p [ ⌊ i k ⌋ ] dp[\ \left\lfloor\frac{i}{k}\right\rfloor\ ] dp[ ⌊ki⌋ ])。所以选择整除运算的更新公式为:
for(int j=1; j<=i; j++)
dp[i] = dp[i]+ dp[i/j];
根据这样的思路可写出以下代码:
/*
MT2147 纸带
*/
#include
using namespace std;
const int MAX = 4e6+5;
int dp[MAX];
int main( )
{
// 录入数据
int n, m;cin>>n>>m;
// 状态初始化
dp[1] = 1;
// 状态转移
for(int i=2;i<=n;i++){
for(int j=1; j<i;j++)
dp[i] = (dp[i]+dp[j])%m;
for(int j=2;j<=i;j++)
dp[i] = (dp[i]+dp[i/j])%m;
}
// 输出结果
cout<<dp[n]<<endl;
return 0;
}
该代码含有两重循环,其时间复杂度为 O ( n 2 ) O\left(n^2\right) O(n2),这在 1 0 6 10^6 106 阶范围超时无疑。因此我们必须想办法优化。
对于自减运算,我们每次都要重复地从 d p [ 1 ] dp[1] dp[1] 开始往后叠加至 d p [ i − 1 ] dp[i-1] dp[i−1] ,这显然是低效的。如果你足够敏感,此时兴许会想起一个数据结构——前缀和。试想,如果有一个单独的数组来记录 “在第 i i i 个数之前的所有 d p [ ] dp[] dp[] 数组之和”,那在求 d p [ i ] dp[i] dp[i] 时就能将 O ( n ) O\left(n\right) O(n) 级的累加操作变为 O ( 1 ) O\left(1\right) O(1) 级的取值操作。即,此时自减运算的更新公式为(设前缀和数组为 p r e f i x [ i ] = ∑ j = 1 i d p [ j ] prefix[i]=\sum_{j=1}^idp[j] prefix[i]=∑j=1idp[j]):
d p [ i ] = p r e f i x [ i − 1 ] dp[i] = prefix[i-1] dp[i]=prefix[i−1]
对于整除运算,这个过程稍微复杂些。假设当前欲求 d p [ i ] dp[i] dp[i],则其除数的取值范围为 [ 2 , i ] [2,\ i] [2, i],对应在执行整除运算后,其值域的取值范围即为 [ 1 , i 2 ] [1,\ \frac{i}{2}] [1, 2i] 。所以其累加的更小的 d p [ ] dp[ ] dp[] 数组范围为: d p [ 1 ] d p [ i 2 ] dp[1] ~ dp[\ \frac{i}{2}\ ] dp[1] dp[ 2i ],因此可得到整除运算的更新公式为:
d p [ i ] = d p [ i ] + p r e f i x [ i 2 ] dp[i] = dp[i]+prefix[\frac{i}{2}] dp[i]=dp[i]+prefix[2i]
如果你也是这么思考的,那很遗憾,这是不正确的。因为你偷吃了一些数据!!!举个例子,假设现在要更新 d p [ 5 ] dp[5] dp[5](即当前i取值为 5),则其除数有: { 2 , 3 , 4 , 5 } \left\{2,\ 3,\ 4,\ 5\right\} {2, 3, 4, 5},对应的取值为: { 2 , 1 , 1 , 1 } \left\{2,\ 1,\ 1,\ 1\right\} {2, 1, 1, 1},所以在取值数组里,其范围虽然为 [ 1 , i 2 ] = [ 1 , 2 ] \left[1,\ \frac{i}{2}\right]=\left[1,2\right] [1, 2i]=[1,2],但他需要累加的却是: d p [ 2 ] + d p [ 1 ] + d p [ 1 ] + d p [ 1 dp[2]+ dp[1]+ dp[1]+ dp[1 dp[2]+dp[1]+dp[1]+dp[1]。因此,整除运算的更新公式和自减运算是有区别的:整除运算必须用一重循环去探测其除数集合中的取值情况并执行统计:
for(int j=2; j<=i; j++)
dp[i] = dp[i]+dp[i/j];
但这样依然没逃出 O ( n 2 ) O\left(n^2\right) O(n2) 的枷锁。
出现这一情况的根本原因在于,整除运算会使结果数组出现重复取值,而统计重复取值又会使求解的时间复杂度和原始办法相差无几,因此这就迫使我们不得不另寻他法。
这时,就需要转换思维了:整除会使结果重叠,乘法可不会!
现在我们做一个大胆的改变。
原题:对整数 n n n 进行若干次自减或整除,以统计从 n n n 降至 1 有多少种不同的运算方式。
等价问题:对整数 1 进行若干次自增或乘法,以统计从 1 升至 n n n 有多少种不同的运算方式。
将原问题转换后,原来的整除运算就变为了乘法,这能给我们的求解带来怎样提升呢?前面提到:整除运算会使结果数组出现重复取值,而存在的重复数据将迫使我们用一重循环单独甄别。所以 “罪魁祸首” 就在整除运算的 “消融性” 上。而乘法却恰好恰好相反,它能将原来各数据的差异进行放大!当然,我们也不需要放大,只要不出现数据重复就行。引入乘法运算对计算效率的提升细节将在下面进行详细阐述。
首先说明问题转换后, d p [ ] dp[ ] dp[] 数组发生的改变。由于原问题是从小往大的方向进行递推,因此 d p [ ] dp[ ] dp[] 数组的更新方向是自左向右;但在转换后(从大往小的方向进行递推),其更新方向就刚好逆转,故定义新的 d p [ ] dp[ ] dp[] 数组含义为: d p [ i ] dp[i] dp[i] 表示从数 i i i 升至数 n n n 有 d p [ i ] dp[i] dp[i] 种不同的运算方式。显然,这时的初始化是置 d p [ n ] = 1 dp[n]=1 dp[n]=1 ,最终待求目标即为 d p [ 1 ] dp[1] dp[1]。其动态转移方程在原理上和前面完全一致,但因为方向的改变,使得 “前缀和” 数组需要重新定义,进而在实现上也出现了差异。前面用前缀和的本意是为了能快速求出 d p [ 1 ] ∽ d p [ i − 1 ] dp[1] \backsim dp[i-1] dp[1]∽dp[i−1] 之和,但现在待求的 d p [ ] dp[ ] dp[] 簇变成了 d p [ i + 1 ] ∽ d p [ n ] dp[i+1] \backsim dp[n] dp[i+1]∽dp[n],故可以建立一个 “后缀和” 数组: b a c k f i x [ i ] = ∑ j = i n d p [ j ] backfix[i]=\sum_{j=i}^ndp[j] backfix[i]=∑j=indp[j],这样我们在进行自增运算时,就能直接从该数组中取出 d p [ i + 1 ] ∽ d p [ n ] dp[i+1] \backsim dp[n] dp[i+1]∽dp[n]。于是,可得到自增运算的更新公式为:
d p [ i ] = b a c k f i x [ i + 1 ] dp[i] = backfix[i+1] dp[i]=backfix[i+1]
乘法运算是一个向后移动的过程,需要考虑其移动后的区间范围以防越界。例如,假设当前 n n n 为 15,目标是更新 d p [ i = 4 ] dp[i = 4] dp[i=4],我们来从这个例子中观察乘法运算需要做出的改变。首先第一点,我们需要遍历乘数,乘数的最小值显然是 2(循环条件为 i ∗ j < = n i*j <= n i∗j<=n),于是过程如下:
如果你也是这么思考的,那很遗憾,这是不完全正确的。因为你又偷吃了一些数据!!!现在站在原问题的角度看,假设当前你处于第 9 个位置,选择除数为 2,那么你的状态转移过程是: 9 → ⌊ 9 2 ⌋ = 4 9\rightarrow\left\lfloor\frac{9}{2}\right\rfloor=4 9→⌊29⌋=4。也就是说,原问题中 “状态:i = 9” 可以转换为 “状态:i = 4”,那么在转换问题后(整除变为乘法),“状态:i = 4” 的下一个状态就可以是 “状态:i = 8、i = 9”,即 d p [ 4 ] dp[4] dp[4] 还应执行 d p [ 4 ] + = d p [ 9 ] dp[4] += dp[9] dp[4]+=dp[9],可上面的执行过程却遗漏了这一点(这种情况在上面的所有乘数情况下都存在)。出现这种情况的原因就是最开始提到的:乘法运算会将原数据的差异放大。那要如何求出乘法运算里某个状态的全部下一状态呢?
分析整除运算的性质,若假设当前处于位置 i i i,由于原问题里 i i i 是通过运算 ⌊ x z ⌋ \left\lfloor\frac{x}{z}\right\rfloor ⌊zx⌋ 得到(此处 x x x 表示原问题里进行除法前的位置, z z z 表示 [ 2 , x ] [2,x] [2,x] 内的某个数,即 x x x 不小于 i i i ,在其之后),则当固定 z z z 的取值时, x x x 的取值范围为 [ i z , i z + z − 1 ] [iz,iz+z-1] [iz,iz+z−1],这就是状态 i i i 到下一个状态的所有可行解。基于这样的思路,可将上面的过程进一步优化为:
提醒一下,对下一个状态区间的求和可通过前面定义的“后缀和”数组在常数级时间内得到,即:
d p [ i z ] + … + d p [ i z + z − 1 ] = b a c k f i x [ i z ] − b a c k f i x [ i z + z ] dp\left[iz\right]+\ldots+dp\left[iz+z-1\right]=backfix\left[iz\right]-backfix\left[iz+z\right] dp[iz]+…+dp[iz+z−1]=backfix[iz]−backfix[iz+z]
下面给出基于新思路得到的完整代码(已 AC):
/*
MT2147 纸袋
*/
#include
using namespace std;
const int MAX = 4e6+5;
long dp[MAX], backfix[MAX];
int main( )
{
// 录入数据
int n, m, tmp;cin>>n>>m;
// 状态初始化
dp[n] = backfix[n] = 1;
// 状态转移
for(int i=n-1; i>=1; i--){
// 加法运算
dp[i] = backfix[i+1];
// 乘法运算
for(int j=2;i*j<=n;j++){
// 防止后面在计算 (i*j+j-1)+1=i*j+j 越界
tmp = min(n, i*j+j-1);
dp[i] = (dp[i] + backfix[i*j] - backfix[tmp+1]) % m;
}
backfix[i] = (backfix[i+1]+dp[i]) % m;
}
// 输出结果
cout<<dp[1]<<endl;
return 0;
}
MT2148 围栏木桩
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
某农场有一个由按编号排列的 n n n 根木桩构成的首尾不相连的围栏。现要在这个围栏中选取一些木桩,按照原有的编号次序排列之后,这些木桩高度成一个升序序列。所谓的升序序列就是序列中的任何一个数都不小于它之前的任何一个数。试编写程序从这个围栏中选取合适的木桩使得选出的木桩个数 t t t 最大,并求出选取出 t t t 根木桩的方案总数 c c c 。
格式
输入格式:文件中的第一行只有一个数 m m m,表明随后有 m m m 个问题的描述信息。
接下来 m m m行,格式为: n , h 1 , h 2 , … , h n (其中 h i ( i = 1 , 2 , 3 , … , n ) n,h_1,h_2,\ldots,h_n(其中\ h_i\left(i=1,2,3,\ldots,n\right) n,h1,h2,…,hn(其中 hi(i=1,2,3,…,n) 表示第 i i i 根木桩的高度)。
输出格式:依次输出每个问题中 t t t 和 c c c 的解,每行输出一个问题的解。样例 1
输入:3
9 10 1 9 8 7 6 3 4 6
3 100 70 102
6 40 37 23 89 91 12输出:4 1
2 2
3 3备注
其中: 1 ≤ m ≤ 5 , 1 ≤ n ≤ 20 , 0 ≤ h i ≤ 150 1\le m\le5,\ 1\le n\le20,\ 0\le h_i\le150 1≤m≤5, 1≤n≤20, 0≤hi≤150。
相关知识点:
动态规划
这道题本质是求 “最长上升子序列”,是动态规划最经典的问题之一。
对于需要进行动态规划的题目而言,其核心点在于如何设计转移方程和转移数组。对于上升序列,其中的元素必定满足后者不小于前者,因此我们在求上升子序列时可以从此角度出发,不断从后往前进行寻找(或从前往后)。基于此,可设转移数组 dp[i] 为 “以索引 i 结尾的子序列中,其含有的上升序列个数”,则最终的待求答案即为:dp[ ] 数组中的最大值。显然,初始情况下,有
d p [ i ] = 1 ( i = 1 , 2 , … , n ) dp\left[i\right]=1\ \left(i=1,2,\ldots,n\right) dp[i]=1 (i=1,2,…,n)
说明:以每个元素结尾的序列都至少有一个 “最长上升子序列”,即由该元素自身构成的单元素序列。
下面通过对一个例子进行推演,以探寻此模型的状态转移方程,观察数组(设索引从1开始):
{ 9 , 10 , 1 , 3 , 4 , 2 , 4 , 1 } \{9,\ 10,\ 1,\ 3,\ 4,\ 2,\ 4,1\} {9, 10, 1, 3, 4, 2, 4,1}
现在需要从左往右枚举每个元素,并寻找以该元素结尾的序列中的“最长上升子序列”。对于每个元素,由于需要寻找其前面比它更小的元素,因此这里还需要一重循环(往前再扫描),以寻找比该元素更小的元素并进行统计。注意:第一个元素(即dp[1] )显然只能取 1,因此扫描的过程从第 2 个元素开始:
至此,整个序列遍历结束,最终 dp[ ] 数组的内容为:{1, 2, 1, 2, 3, 2, 4, 2}(其中最大取值为 4 ),故认为该序列中的最长递增子序列长度为 4。
综上,可得到该模型的状态转移方程为(其中,ary[ ]为原序列数组,i, j 分别为遍历的外、内层循环):
if(ary[i] >= ary[j])
dp[i] = max(dp[i], dp[j]+1);
在代码里的具体实现如下
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
if(ary[j] <= ary[i])
dp[i] = max(dp[j]+1,dp[i]);
但对本题而言,任务尚未结束,其还要求输出最大增序序列的方案总数。这要如何计算?考虑如下数列:
{ 40 , 37 , 23 , 89 , 91 , 12 } \{40,\ 37,\ 23,\ 89,\ 91,\ 12\} {40, 37, 23, 89, 91, 12}
不难发现,该序列的最长子序列长度为 3,分别有:{40, 89, 91}、{37, 89, 91}、{23, 89, 91},共 3 种组合方案。这些方案都有一个特点:组合数等于各唯一端口之间通道取值的数量之积。你可能会问,什么是唯一端口
,什么是可变通道
?以上面的数列为例,不难发现在各组合方案中,显然 89、91 都是固定的,因此称其为唯一端口(即当前数量的序列中,这个位置只能取到这一个元素,不可能为其他值);其余位置则为非固定的,因此称其为可变通道(即这个位置上的元素取值有 2 个及以上)。
所以,对于原数列 { 40 , 37 , 23 , 89 , 91 , 12 } \{40,\ 37,\ 23,\ 89,\ 91,\ 12\} {40, 37, 23, 89, 91, 12} ,其最长(长度为 3 )子序列里只有 1 个通道,该通道取值有 {40,37,23} 3 个,因此其最长子序列的组合方案个数为 3。数列 {2,5,3,7,54,31,21,69},其最长(长度为 5 )子序列里有 2 个通道(通道取值分别为 {5, 3}、{54,31, 21}),因此其最长子序列的组合方案个数为 2×3=6 种(分别为 {2, 5, 7, 54, 69}、{2, 5, 7, 31, 69}、{2, 5, 7, 21, 69}、{2, 3, 7, 54, 69}、{2, 3, 7, 31, 69}、{2, 3, 7, 21, 69})。
知道原理后,我们来思考如何编码实现。
首先要知道第一件事,递增子序列的方案数也是随元素总数发生改变的。所以我们应该在求 dp[ ] 数组的同时,求解对应情况下(准确地说,是当前最长子序列的情况下)的方案数。于是,定义num[i] 为“以索引 i 结尾的子序列中,其最长上升序列个数 dp[i] 的方案数”。
同样地,初始情况下若视每个元素构成一个单元素序列,则每个以索引i结尾的子序列的(长度为dp[i]=1)的方案数均为1。所以初始情况下,有
n u m [ i ] = 1 ( i = 1 , 2 , … , n ) num\left[i\right]=1\ \left(i=1,2,\ldots,n\right) num[i]=1 (i=1,2,…,n)
现在我们遍历数列 { 40 , 37 , 23 , 89 , 91 , 12 } \{40,\ 37,\ 23,\ 89,\ 91,\ 12\} {40, 37, 23, 89, 91, 12},试图在求 dp[ ] 数组的同时完善 num[ ] 数组的内容:
i = 2 时(元素取值:37),dp[ ]={1,1,1,1,1,1},num[ ]={1,1,1,1,1,1}。其前面不存在任何不大于其本身的元素,则“以 i=2 结尾的序列的最长子序列”即为由当前元素本身构成的单元素序列,即 dp[2] = 1。自然地,“以 i=2 结尾的序列的最长子序列”的方案数也不变,即 num[2] = 1。
i = 3 时(元素取值:23),dp[ ]={1,1,1,1,1,1},num[ ]={1,1,1,1,1,1}。其前面不存在任何不大于其本身的元素,则“以 i=3 结尾的序列的最长子序列”即为由当前元素本身构成的单元素序列,即 dp[3] = 1。自然地,“以 i=3 结尾的序列的最长子序列”的方案数也不变,即 num[3] = 1。
i = 4 时(元素取值:89),dp[ ]={1,1,1,1,1,1},num[ ]={1,1,1,1,1,1}。其前面存在 3 个不大于其本身的元素,故分别讨论(说明可能存在通道)。
① 对于第 1 个元素(ary[1] = 40),该元素能加入并拓展当前序列长度,因此有dp[4] = dp[1]+1 = 2。同时,由于dp[4] 进行了更新,这表明“从目前看来,加入元素所处位置仅有一个取值,故认定此位置是一个端口”。从前面的分析可知,端口对序列的组合方案无影响,因此置num[4] = num[1] = 1;
② 对于第 2 个元素(ary[2] = 37),该元素能加入并拓展当前序列长度,但是现在dp[2] +1= 2 = dp[4],无需对dp[4] 再进行更新,这表明“加入元素所处位置存在2个及以上的元素可填充,故认定此位置是一个通道”。从前面的分析可知,通道对序列的组合方案有影响(方案数为通道取值的数量之积)。但是我们并不需要单独设值来记录通道的取值,因为在迭代过程中按序统计每个通道的取值时(即对num数组的每个元素按序累加时),其会将前面每一个累加的结果传播至num数组后面的元素中(本质上就是进行了乘数)。所以对于出现通道的情况,直接记num[i]++即可(这个过程可参照着代码和实际例子进行理解)。故置num[4] ++(现在num[4] =2);
③ 对于第 3 个元素(ary[3] = 23),该元素能加入并拓展当前序列长度,但是现在dp[3]+1 = 2 = dp[4],无需对dp[4] 再进行更新,这表明“加入元素所处位置存在2个及以上的元素可填充,故认定此位置是一个通道”。根据前面的分析,直接置num[4] ++(现在num[4] =3);
遍历结束。这就是说:以 i = 4 结尾的子序列中,最长子序列长度为2,共有 3 种组合方案(分别为{40, 89}、{37, 89}、{23, 89})。
i = 5 时(元素取值:91),dp[ ]={1,1,1,2,1,1},num[ ]={1,1,1,3,1,1}。其前面存在 4 个不大于其本身的元素,故分别讨论(说明可能存在通道)。
① 对于第 1 个元素(ary[1] = 40),该元素能加入并拓展当前序列长度,因此有dp[5] = dp[1]+1 = 2。同时,由于dp[5] 进行了更新,这表明“从目前看来,加入元素所处位置仅有一个取值,故认定此位置是一个端口”。从前面的分析可知,端口对序列的组合方案无影响,因此置num[5] = num[1] = 1;
② 对于第 2 个元素(ary[2] = 37),该元素能加入并拓展当前序列长度,但是现在dp[2] +1= 2 = dp[5],无需对dp[5] 再进行更新,这表明“加入元素所处位置存在2个及以上的元素可填充,故认定此位置是一个通道”,则置num[5] ++(现在num[5] =2);
③ 对于第 3 个元素(ary[3] = 23),该元素能加入并拓展当前序列长度,但是现在dp[3]+1 = 2 = dp[5],无需对dp[5] 再进行更新,这表明“加入元素所处位置存在2个及以上的元素可填充,故认定此位置是一个通道”,则置num[5] ++(现在num[5] =3);
④ 对于第 4 个元素(ary[2] = 89),该元素能加入并拓展当前序列长度,且dp[4] +1= 3 > dp[5],故更新dp[5] = dp[4]+1 = 3,这表明现在找到了一个新端口(目前暂认为),则置num[5] = num[4] = 3)。
遍历结束。这就是说:以i = 5结尾的子序列中,最长子序列长度为3,共有3种组合方案(分别为{40, 89, 91}、{37, 89, 91}、{23, 89, 91})。
i = 6 时(元素取值:12),dp[ ]={1,1,1,1,1,1},num[ ]={1,1,1,1,1,1}。其前面不存在任何不大于其本身的元素,则“以i=6结尾的序列的最长子序列”即为由当前元素本身构成的单元素序列,即dp[6] = 1。自然地,“以i=6结尾的序列的最长子序列”的方案数也不变,即num[6] = 1。
至此,整个序列遍历结束,此时num[ ] 数组中存放的就是每个位置在以该位置上的元素结尾的子序列中,其最长序列的组合方案数。
根据以上思路可写出求解该题的完整代码(已 AC):
/*
MT2148 围栏木桩
*/
#include
using namespace std;
const int MAX = 25;
int ary[MAX], dp[MAX], num[MAX];
int main( )
{
// 录入数据
int m, n;cin>>m;
while(m--){
cin>>n;
for(int i=1;i<=n;i++){
cin>>ary[i];
dp[i] = num[i] = 1;
}
// 状态转移
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++){
if(ary[j] <= ary[i])
if(dp[j]+1 > dp[i]){
dp[i] = max(dp[j]+1, dp[i]);
num[i] = num[j];
}
else if(dp[j]+1 == dp[i])
num[i]++;
}
// 寻找 dp, num 数组中的最大值
int ansT = dp[1], ansC = 0;
for(int i=2;i<=n;i++)
ansT = max(ansT, dp[i]);
for(int i=1;i<=n;i++)
if(dp[i] == ansT)
ansC += num[i];
// 输出结果
cout<<ansT<<" "<<ansC<<endl;
}
return 0;
}
MT2152 抽奖
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
小码哥在集市上逛街,遇见了抽奖活动,抽一次 2 元,但是可能会抽出 1,2,3,4四种情况,他们是等概率的。
小码哥计划抽 n n n 次,问亏本的概率是多少(即得到的奖金小于本金),小码哥赚了超过一半本金的概率是多少(赚到的钱是奖金-本金后的部分)?格式
输入格式:输入 n n n 表示小码哥连抽的次数;
输出格式:第一行输出吃亏的概率。
第二行输出赚超过一半本金的概率。
概率用最简分数表示,具体看样例样例1
输入:2
输出:3/16
3/16备注
其中: 1 ≤ n ≤ 30 1\le n\le30 1≤n≤30。
相关知识点:
动态规划
抽奖一次的本金是 2 元,则抽奖 n n n 次的本金为 2 n 2n 2n 元;抽奖的收益为1,2,3,4元(等概率),则抽奖 n n n 次的收益范围为 n 4 n n~4n n 4n (注:收益为正整数)。所以,亏本(奖金小于本金)时对应的收益范围为 [ n , 2 n ) [n,\ 2n) [n, 2n) ,赚超过一半本金时的收益范围为 ( 3 n , 4 n ] (3n,4n] (3n,4n]。现要求这两种情况出现的概率,即要算出现这两种情况各自的组合数,并将其与总收益范围出现的组合数做比即可。所以,现在我们的目标是:统计收益范围为 [ n , 2 n ) [n,\ 2n) [n, 2n) 和 ( 3 n , 4 n ] (3n,4n] (3n,4n] 的组合方案总数。很明显,不同抽奖次数之间存在递推关系(后续抽奖的收益总额基于前面),因此这就需要进行动态规划。
考虑到与递推关系相关的两个参数:抽奖次数、收益,因此可设该题的状态数组为dp[i][j],表示“抽奖i次得到总额为j的方案数”。考虑某一次抽奖 dp[i][j],其只能从前一次 dp[i-1][x] 递推而来,而前一次的收益总额x只能是4种情况,即:j-1、j-2、j-3、j-4。于是便得到了该题的状态转移方程:
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j − 2 ] + d p [ i − 1 ] [ j − 3 ] + d p [ i − 1 ] [ j − 4 ] dp[i][j]= dp[i-1][j-1]+ dp[i-1][j-2]+ dp[i-1][j-3]+ dp[i-1][j-4] dp[i][j]=dp[i−1][j−1]+dp[i−1][j−2]+dp[i−1][j−3]+dp[i−1][j−4]
最后考虑边界条件。在抽奖最开始时,由于收益为1,2,3,4元的情况都是等概率的,因此最初时 dp[1][1]= dp[1][2]= dp[1][3]= dp[1][4]=1。
在得到状态转移方程的转移条件和边界条件后便能计算整个dp[][]数组的取值。接下来,就能通过一层循环分别统计收益在 [ n , 2 n ) [n,\ 2n) [n, 2n) 和 ( 3 n , 4 n ] (3n,4n] (3n,4n] 的组合方案数。
总的组合方案数是多少?抽取 n n n 次,每次有4种可能,则全部可能的组合方案数为 4 n 4^n 4n。
如何表示最简分数?最简分数需要用到分子和分母的最大公约数,将分子和分母除以最大公约数后,该分数即为原分式的最简分数。求最大公约数用辗转相除法即可。
下面直接给出求解本题的完整代码:
/*
MT2152 抽奖
注意数据范围,用 int 只能过一半的数据
*/
#include
using namespace std;
const int MAX = 35;
long dp[MAX][MAX*MAX];
// 求最大公约数
int gcd(long a, long b)
{ return (b==0)?a:gcd(b, a%b); }
int main()
{
// 录入数据
long n, sum1 = 0, sum2 = 0;
cin>>n;
// 初始化状态数组
dp[1][1] = dp[1][2] = dp[1][3] = dp[1][4] = 1;
// 状态转移
for(int i=1;i<=n; i++)
for(int j=i; j<=4*n; j++)
for(int k=1; k<=4; k++)
if(j>k)
dp[i][j] += dp[i-1][j-k];
// 计算亏本的总体情况
for(int i=n; i<2*n; i++)
sum1 += dp[n][i];
// 计算超过一半本金的情况
for(int i=3*n+1; i<=4*n; i++)
sum2 += dp[n][i];
// 计算所有可能的情况
long SUM = pow(4,n);
// 输出(最简分数)
long tmp = gcd(sum1, SUM);
cout<<(sum1/tmp)<<"/"<<(SUM/tmp)<<endl;
tmp = gcd(sum2, SUM);
cout<<(sum2/tmp)<<"/"<<(SUM/tmp)<<endl;
return 0;
}
MT2153 异或和
难度:钻石 时间限制:1秒 占用内存:128M
题目描述
给定一个长度为 n n n 的序列 A = { a 1 , a 2 , … , a n } A=\left\{a_1,\ a_2,\ {\ldots,\ a}_n\right\} A={a1, a2, …, an} ,寻找在 A A A 的所有递增子序列(可以为空)的异或和中出现的数。
格式
输入格式:第一行一个正整数 n n n 表示序列长度;
第二行 n n n 个整数表示序列 A A A;
输出格式:第一行输出满足要求的数的个数。
第二行从小到大输出在 A A A 的所有递增子序列的异或和中出现的数;样例 1
输入:4
4 2 2 4输出:4
0 2 4 6样例 2
输入:2
1 5输出:4
0 1 4 5样例 3
输入:2
5 1输出:3
0 1 5备注
其中: 1 ≤ n ≤ 1 e 5 , 1 ≤ a i ≤ 500 1\le n\le1e5,\ 1\le a_i\le500 1≤n≤1e5, 1≤ai≤500。
相关知识点:
动态规划
求解本题首先要知道 “异或” 运算是什么(在计算机中的运算符号为 ^
)。简单说就是“将数据转换为二进制后,逐位比较,相同为0不同为1”。例如:
1^2 = (0001)^(0010) = (0011) = 3;
2^12 = (0010)^(1100) = (1110) = 14;
5^14 = (0101)^(1110) = (1011) = 11
本题要求给定序列中,所有递增子序列的全部元素在执行异或运算后得到的值。例如,对于题目给出的序列 ary = {4, 2, 2, 4}。
第一步,算出所有的递增子序列:{}, {4}, {2}, {2, 4}(空序列在题目中被视作数字0)。
第二步,算出每个子序列中的全部元素在执行异或运算后的值:0, 4, 2, 6。
因此最终输出的全部异或值为:0, 4, 2, 6,总数为4。
这里有个难点,递增子序列。从题目的需求来看,我们肯定要求出所有递增子序列并统计该序列中所有值的异或运算结果。但是,从数据范围来看必定超时无疑。因此,必须想办法加速统计过程。
实际上,从题目的数据范围看: 1 ≤ a i ≤ 500 < 512 = 2 9 1\le a_i\le500<512=2^9 1≤ai≤500<512=29 ,不论给出的序列多长,所有可能的序列组合方式中的全部数值在进行异或运算后,能得到的最大取值为 2 9 − 1 = 511 2^9-1\ =511 29−1 =511。因此,最终得到的所有异或结果不会超过 511 种可能。为此,可定义 d p [ i ] dp[i] dp[i] 数组表示 “目前子序列中全部元素的异或结果为i的最小子序列结尾元素” 。例如,现在存在三个递增子序列: { p 1 , p 2 , … , p x } , { q 1 , q 2 , … , q y } , { r 1 , r 2 , … , r z } \left\{p_1,p_2,\ldots,p_x\right\},\ \ \left\{q_1,q_2,\ldots,q_y\right\},\ \ \left\{r_1,r_2,\ldots,r_z\right\} {p1,p2,…,px}, {q1,q2,…,qy}, {r1,r2,…,rz},它们中全部元素的异或结果均为 i i i(即 p1 ^ p2 ^ … ^ px= q1 ^ q2 ^ … ^ qy = r1 ^ r2 ^ … ^ rz = i ),且 p x < q y < r z p_x
if(dp[i] < ary[index])
dp[i^ary[index]] = min(dp[i^ary[index]], ary[index])
它表示,当原序列中存在一个大于 d p [ i ] dp[i] dp[i] 的元素时,就将当前元素与之前得到的序列元素异或值进行异或运算,并保留更小的元素值作为 dp[i^ary[index]] 的值,从而进行状态转移。采取这种方法,我们只需要两重循环,外层(循环遍历变量为 i n d e x index index)遍历原序列 a r y ary ary,内层(循环遍历变量为 i i i)遍历 512 种可能的取值。从而将时间复杂度降至线性。
与前面所有动态规划题目不同的是,这一次,我们不直接将待求的结果放在 dp[ ] 数组中,而是转存当前这种状态下能不能存在一个子序列满足给定条件。然后在整个 dp[ ] 数组被更新完毕后,统计其中“被更新过”的元素数量。
这道题的难度是非常大的,特别是状态转移数组的构建,非常不容易想到。
下面给出求解该题的完整代码(已 AC):
/*
MT2153 异或和
*/
#include
using namespace std;
const int MAX = 1e5+5;
const int MAXA = 520;
const int INF = 0x3f3f3f3f;
int a[MAX], dp[MAX];
int main( )
{
// 初始化状态数组
memset(dp,INF,sizeof(dp));
dp[0] = 0;
// 录入数据
int n; cin>>n;
for(int i=1; i<=n; i++)
cin>>a[i];
// 状态转移
for(int i=1; i<=n; i++)
for(int j=0; j<= MAXA; j++)
if(dp[j] < a[i])
dp[j^a[i]] = min(dp[j^a[i]], a[i]);
// 统计状态数组内被更新过的元素
vector<int> ans;
for(int i=0; i<=MAXA; i++)
if(dp[i] != INF)
ans.push_back(i);
// 输出结果
cout<<ans.size()<<endl;
for(int val: ans)
cout<<val<<" ";
return 0;
}
MT2154 海龟
难度:钻石 时间限制:1秒 占用内存:128M
题目描述
很多人把LOGO编程语言和海龟图形联系起来。在这种情况下,海龟沿着直线移动,接受命令
T
(转向180度)和F
(向前移动 1 单元)。
你会收到一份给海龟的命令清单。你必须从列表中精确地改变 n n n 个命令(一个命令可以被改变多次)。要求出海龟在遵循修改后的所有命令后,会从起点最远可以移到多远?格式
输入格式:第一行为命令清单;
第二行为需要改变的命令数。
输出格式:一行即为答案。样例 1
输入:FFFTFFF
2输出:6
备注
字符串非空且长度不大于 100, 1 ≤ n ≤ 50 1\le n\le50 1≤n≤50。
相关知识点:
动态规划
在这道题里,转向操作F为180度,实际上就是逆转方向。因此在本题中,我们可将海龟的移动空间视为一条直线,移动方式视为前进与后退即可。接着要弄清楚走最远的的含义。比如一份命令:FFTFF
,
现在题目的要求是,给定命令清单和修改次数,让你求出海龟的最远移动距离。
实际上,这也是一道典型的动态规划题目。我们可以依次扫描命令进行状态转移,试图寻找在指定修改次数中能使移动距离最远的方案。显然,这个修改过程涉及到三个参数(会对结果产生影响):当前扫描到的命令位置、当前已经修改了多少次命令以及当前海龟的朝向。据此,可定义本题的状态数组为 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k],表示 “当前i个命令一共执行了j次修改且其朝向为k时,此海龟将移动 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 的距离”,显然 k 只有两种取值,要么朝前要么朝后(分别定义为 0 和 1)。
接下来我们讨论其状态转移方程(为便于分析,假设命令清单为字符串数组 o p t s [ ] opts[] opts[])。若现在扫描到第 i i i 个命令(字符): o p t s [ i ] opts[i] opts[i],且执行了 j j j 次修改,现在需要求出 d p [ i ] [ j ] [ 0 ] dp[i][j][0] dp[i][j][0] 和 d p [ i ] [ j ] [ 1 ] dp[i][j][1] dp[i][j][1] 的最大取值。显然,该值与其前一次状态( d p [ i − 1 ] dp[i-1] dp[i−1])有关。具体地说,要基于 d p [ i − 1 ] dp[i-1] dp[i−1] 在 j j j 种不同修改规则情况下得到的距离(即 d p [ i − 1 ] [ 0 ] ∽ d p [ i − 1 ] [ j ] dp[i-1][0] \backsim dp[i-1][j] dp[i−1][0]∽dp[i−1][j])以得到 d p [ i ] [ j ] dp[i][j] dp[i][j] 的最大值,且每一种还需要额外考虑两种不同的朝向(即 d p [ i − 1 ] [ ] [ 0 ] dp[i-1][][0] dp[i−1][][0] 和 d p [ i − 1 ] [ ] [ 1 ] dp[i-1][][1] dp[i−1][][1])。因此,这就需要分情况讨论(注意一件事,修改命令只有两种选择:T 或者 F,因此对修改操作而言,它的奇偶性才是决定是否会发生变化的关键),若假设从前一次状态到当前状态共修改了 r 次命令,则讨论:
d p [ i ] [ j ] [ 0 ] = m a x ( d p [ i ] [ j ] [ 0 ] , d p [ i − 1 ] [ j − r ] [ 1 ] ) ; d p [ i ] [ j ] [ 1 ] = m a x ( d p [ i ] [ j ] [ 1 ] , d p [ i − 1 ] [ j − r ] [ 0 ] ) ; dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-r][1]); \\ dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-r][0]); dp[i][j][0]=max(dp[i][j][0],dp[i−1][j−r][1]);dp[i][j][1]=max(dp[i][j][1],dp[i−1][j−r][0]);
若当前命令 o p t s [ i ] = T opts[i] = T opts[i]=T,且 r r r 为奇数。这时,第 i i i 个命令会发生变化(变为 F),即现在状态和前一状态不会出现方向的逆转,而是产生距离的差异。因此,无论前一状态方向如何,其总是会在该方向的基础上增加1的距离。若前一状态方向朝前,则+1;若方向朝后,则-1。因此得到这种情况下的状态转移方程:
d p [ i ] [ j ] [ 0 ] = m a x ( d p [ i ] [ j ] [ 0 ] , d p [ i − 1 ] [ j − k ] [ 0 ] + 1 ) ; d p [ i ] [ j ] [ 1 ] = m a x ( d p [ i ] [ j ] [ 1 ] , d p [ i − 1 ] [ j − k ] [ 1 ] − 1 ) ; dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][0]+1); \\ dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][1]-1); dp[i][j][0]=max(dp[i][j][0],dp[i−1][j−k][0]+1);dp[i][j][1]=max(dp[i][j][1],dp[i−1][j−k][1]−1);
若当前命令 o p t s [ i ] = F opts[i] = F opts[i]=F,且 r r r 为偶数。这时,第 i i i 个命令并不会发生变化,即现在状态和前一状态不会出现方向的逆转,而是产生距离的差异。对当前状态的两个方向:当方向朝前时,其前一状态的方向就朝后;当方向朝后时,其前一状态的方向就朝前。因此得到这种情况下的状态转移方程:
d p [ i ] [ j ] [ 0 ] = m a x ( d p [ i ] [ j ] [ 0 ] , d p [ i − 1 ] [ j − k ] [ 0 ] + 1 ) ; d p [ i ] [ j ] [ 1 ] = m a x ( d p [ i ] [ j ] [ 1 ] , d p [ i − 1 ] [ j − k ] [ 1 ] − 1 ) ; dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][0]+1); \\ dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][1]-1); dp[i][j][0]=max(dp[i][j][0],dp[i−1][j−k][0]+1);dp[i][j][1]=max(dp[i][j][1],dp[i−1][j−k][1]−1);
d p [ i ] [ j ] [ 0 ] = m a x ( d p [ i ] [ j ] [ 0 ] , d p [ i − 1 ] [ j − k ] [ 0 ] ) ; d p [ i ] [ j ] [ 1 ] = m a x ( d p [ i ] [ j ] [ 1 ] , d p [ i − 1 ] [ j − k ] [ 0 ] ) ; dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][0]); \\ dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][0]); dp[i][j][0]=max(dp[i][j][0],dp[i−1][j−k][0]);dp[i][j][1]=max(dp[i][j][1],dp[i−1][j−k][0]);
dp数组的边界条件是 d p [ 0 ] [ 0 ] [ 0 ] = d p [ 0 ] [ 0 ] [ 1 ] = 0 dp[0][0][0] = dp[0][0][1] = 0 dp[0][0][0]=dp[0][0][1]=0,即对空命令不进行任意变化时,海龟移动的最远距离为 0。在求出 dp 数组后,最终的最远距离只会出现在 d p [ s i z e ] [ n ] [ 0 ] 和 d p [ s i z e ] [ n ] [ 1 ] dp[size][n][0]和dp[size][n][1] dp[size][n][0]和dp[size][n][1] 中(其中,size 表示命令的长度,n 表示修改的总次数)。
下面给出求解本题的完整代码(已 AC):
/*
MT2154 海龟
注意数据范围,这里必须用 long
*/
#include
using namespace std;
const int MAXL = 105;
const int MAXN = 55;
const int INF = 0x3f3f3f3f;
long dp[MAXL][MAXN][2];
int main()
{
// 录入数据
string str; int n;
cin>>str>>n;
// 获取操作指令(字符串)长度
int size = str.size();
// 为便于后续使用该指令时的索引从 1 开始,对该指令进行处理
str = " " + str;
// 初始化状态数组
for(int i=0; i<=size;i++)
for(int j=0; j<=n; j++)
dp[i][j][0] = dp[i][j][1] = -INF;
dp[0][0][0] = dp[0][0][1] = 0;
// 状态转移
for(int i=1;i<=size; i++)
for(int j=0; j<=n; j++)
for(int k=0; k<=j; k++)
if(str[i] == 'F'){
if(k&1 == 1){
dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][0]);
dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][0]);
}else{
dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][0]+1);
dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][1]-1);
}
}else if(k&1 == 1){
dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][0]+1);
dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][1]-1);
}else{
dp[i][j][0] = max(dp[i][j][0], dp[i-1][j-k][1]);
dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-k][0]);
}
// 输出结果
cout<<max(dp[size][n][0], dp[size][n][1])<<endl;
return 0;
}