一、走方格(DP,2020)
题目描述
在平面上有一些二维的点阵。 这些点的编号就像二维数组的编号一样,从上到下依次为第 1 至第 n 行, 从左到右依次为第 1 至第 m 列,每一个点可以用行号和列号来表示。 现在有个人站在第 1 行第 1 列,要走到第 n 行第 m列。只能向右或者向下走。 注意,如果行号和列数都是偶数,不能走入这一格中。 问有多少种方案。
输入格式
输入一行包含两个整数 n, m。
输出格式
输出一个整数,表示答案。
评测用例规模与约定
对于所有评测用例,1 ≤ n ≤ 30, 1 ≤ m ≤ 30。
思路
让我们先画一个以下的图来直接分析。
![](https://i-blog.csdnimg.cn/blog_migrate/44c37a3e878a60144fa3a3abf68e06d8.png)
集合的状态表示还是比较好想的,因为我们是在一个二维矩阵中走方格且题目对行编号和列编号都有限制,所以状态表示是一个二维的。
属性:一般题目问的是什么,我们的属性就是什么。本题问的是方案的数量,则属性就是方案的数量。
状态转移:先假设我们走到了一个一般的点,如f[i][j];一般我们进行状态转移的时候都是考虑其最后一步怎么走,即是从哪个地方走到f[i][j]这个点的。题目要求说,走方格时我们只能往右或往下走。因此,f[i][j]有两种可能,一种时从它的上方f[i - 1][j]向下走到f[i][j];另一种是从它的左边f[i][j - 1]向右走到f[i][j]。
状态计算:f[i][j] = f[i - 1][j] + f[i][j - 1]。
这道题目还要求如果行号和列号都为偶数的话,我们不能走入这一格中,因此我们只需要在枚举的时候特判以下就能解决这个问题。
最后,我们需要考虑一下初值的问题。f[1][1] = 1。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 50;
int n, m;
int f[N][N];
int main()
{
cin >> n >> m;
f[0][1] = 1;
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
{
if(i % 2 == 0 && j % 2 == 0) f[i][j] = 0;
else f[i][j] = f[i][j - 1] + f[i - 1][j];
}
cout << f[n][m] << endl;
return 0;
}
/*
f[0][1] = 1;
f[1][1] = f[1][0] + f[0][1];
*/
二、矩阵(DP,2020)
题目描述
把 1 ∼ 2020 放在 2 × 1010 的矩阵里。要求同一行中右边的比左边大,同一 列中下边的比上边的大。一共有多少种方案?
答案很大,你只需要给出方案数除以 2020 的余数即可。
思路
这道题目就比上面那道题目要稍微难想一点,因此我们可以先模拟一下,在矩阵中填数的这样一个过程。
![](https://i-blog.csdnimg.cn/blog_migrate/f95378c3bff51e0c06d011d292d88e13.png)
为什么5只有一个位置摆放了?
因为题目要求,同一行中右边的比左边的大,下边比上边大。设想一下,假设5摆放在图中蓝色圈圈的下面,那么还有哪一个比5小的数能摆在它的上面。
由这个图我们可以抽象出一个规律:
a. 当上面摆放的数的数量和下面摆放的数的数量一样多的时候,下一个数只有一种选择;则当上面摆放的数的数量多余下面摆放的数的数量时我们的下一个数有两种选择。
b. 且上面摆放的数的数量一定是会大于等于下面的数的数量的,即在我们摆放的过程中,下面的数是永远都不可能比上面的数多的。
c. 不知道有没有小伙伴注意到,在模拟的过程中,我们是按1~n的顺序依次摆放这些数的。我觉得这题告诉我们,我们必须得先手动模拟一下,且在模拟的过程中,不是随便模拟的,而是遵循着一定的规律。不然,即使我们确实是在手动模拟,也很难推出问题的本质。
![](https://i-blog.csdnimg.cn/blog_migrate/ad3a6bbe26fcfab2f09f386e6713e350.png)
虽然我们图中画了两类集合,但是在某些情况下,并不是两类集合都存在的。
首先,需要明确j <= i的。
当j < i时,我们有两种位置可以选择。因此,f[i][j] = f[i - 1][j] + f[i][j - 1]。
当j = i时,也就是说我们摆放完最后一个位置后,j和i相等了,因此这个数只可能摆放在下面那一行。因此,f[i][j] = f[i][j - 1]。
注:上面的分析中我们是在进行正推,而此处我们需要考虑最后一步,是在倒推,大家不要混淆了。
初始化操作:
f[0][i] = 0, f[i][0] = 1;
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int f[1011][1011];
int main()
{
//初始化
for(int i = 1; i <= 1010; i ++ ) f[i][0] = 1;
for(int i = 1; i <= 1010; i ++ )
for(int j = 1; j <= i; j ++ )
{
if(i == j) f[i][j] = f[i][j - 1];
else f[i][j] = (f[i - 1][j] + f[i][j - 1]) % 2020;
}
cout << f[1010][1010] << endl;
return 0;
}
三、测试次数(DP,2018)
题目描述
本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。
X 星球的居民脾气不太好,但好在他们生气的时候唯一的异常举动是:摔手机。
各大厂商也就纷纷推出各种耐摔型手机。x星球的质监局规定了手机必须经过耐摔测试,并且评定出一个耐摔指数来,之后才允许上市流通。
X 星球有很多高耸入云的高塔,刚好可以用来做耐摔测试。塔的每一层高度都是一样的,与地球上稍有不同的是,他们的第一层不是地面,而是相当于我们的 2楼。
如果手机从第 7层扔下去没摔坏,但第 8层摔坏了,则手机耐摔指数= 7。 特别地,如果手机从第 1层扔下去就坏了,则耐摔指数 = 0 。 如果到了塔的最高层第 n层扔没摔坏,则耐摔指数 = n。
为了减少测试次数,从每个厂家抽样 3部手机参加测试。
某次测试的塔高为 1000层,如果我们总是采用最佳策略,在最坏的运气下最多需要测试多少次才能确定手机的耐摔指数呢?
请填写这个最多测试次数。
思路
状态表示:f[i][j],表示i层楼j部手机最多需要测试的次数。(这一步其实是将问题的规模缩小了)
状态转移方程:
i层楼j部手机,我们是将手机一部一部的扔掉。
对于第一部手机,我们有i中仍法(从1楼仍,从2楼仍,……,从第i楼仍)
对于第k中仍法,即从第k楼仍,有两种可能:“摔坏”和“没摔坏”。
假设“摔坏”,后面只需要测k层以下的楼层:f[i][j] = f[k - 1][j - 1] + 1。(这里手机也少了一部)
假设“没摔坏”,后面只需要测k层以上的楼层:f[i][j] = f[i - k][j] + 1。
根据题意,这里的“摔坏”和“没摔坏”两种可能究竟是哪个就是你的运气了,因为题目要求是最坏运气,因此我们总是选择测试次数大的那一个。
即f[i][j] = max(f[k - 1][j - 1] + 1, f[i - k][j] + 1)。
又因为采取的是“最佳策略”,因此,我们在i中仍法中选择一个最小值。
即:f[i][j] = min(for k in i : max(f[k - 1][j - 1] + 1, f[i - k][j] + 1))
初值考虑
f[1][i] = 1(一层楼只需要测试一次)
f[i][1] = i(i层楼1部手机需要测试i次)
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int f[1001][4];
int main()
{
for(int i = 1; i <= 3; i ++ ) f[1][i] = 1;
for(int i = 1; i <= 1000; i ++ ) f[i][1] = i;
for(int i = 2; i <= 1000; i ++ ) //楼层数
for(int j = 2; j <= 3; j ++ ) //手机部数
{
int temp = 0;
f[i][j] = 0x3f3f3f3f;
for(int k = 1; k <= i; k ++ )
{
temp = max(f[k - 1][j - 1] + 1, f[i - k][j] + 1);
f[i][j] = min(f[i][j], temp);
}
}
cout << f[1000][3] << endl;
return 0;
}
四、k倍区间(前缀和、同余,2017)
题目描述
给定一个长度为 N的数列,,
,……,
,如果其中一段连续的子序列
,
,……,
( i <= j) 之和是 K的倍数,我们就称这个区间 [i, j] 是 K 倍区间。
你能求出数列中总共有多少个 K倍区间吗?
输入描述
第一行包含两个整数 N和 K (1 <= N, K <= )。 以下 N 行每行包含一个整数
(1 <=
<=
)
输出描述
输出一个整数,代表 K 倍区间的数目。
思路
暴力(TLE)。我们很容易就能想到用一个双层循环来寻找一段连续的子序列,然后再用一层循环来将连续的子序列相加求和,最后再判断一下和是否是k的倍数。这里,我在这个暴力的基础上,做了前缀和的优化,因为求某一段连续区间的和其实很容易让我们想到前缀和算法。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, k, res;
LL s[N];
int main()
{
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i ++ )
{
int a;
scanf("%d", &a);
s[i] = s[i - 1] + a;
}
for(int i = 1; i <= n; i ++ )
for(int j = i; j <= n; j ++ )
{
LL m = s[j] - s[i - 1];
if(m % k == 0) res ++;
}
printf("%d\n", res);
return 0;
}
同余优化。
上一个方法是用两层循环来寻找子序列,i和j都是在变化着的。
这里枚举每一个右端点,
,……,
。
对于每一个右端点,先求以
作为右端点的区间有多少个区间和是k的倍数。
由于要求区间和,因此我们先预处理出来前缀和数组s[]。
则以为右端点的区间和分别是
-
,
-
,
-
,……,
-
。
若某个区间[,
]的区间和是K的倍数,即
-
0 (mod k),也就是
(mod k),即
和
除以k同余。
因此我们只需要统计 ~
中有多少个数与
同余,就可以知道以
作为右端点的区间有多少个区间和是k的倍数。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, k;
LL s[N], cnt[N], res;
int main()
{
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i ++ )
{
int a;
scanf("%d", &a);
s[i] = s[i - 1] + a;
}
cnt[0] = 1;
for(int i = 1; i <= n; i ++ )
{
res += cnt[s[i] % k]; //先看一下与s[i]%k同余的数有多少个,有多少个就代表有多少个K倍区间
cnt[s[i] % k] ++; //然后再将cnt ++
}//如果不懂这两行代码的同学建议手动模拟一下这个过程
printf("%lld\n", res);
return 0;
}
五、包子凑数(DP、数论,2017)
题目描述
小明几乎每天早晨都会在一家包子铺吃早餐。他发现这家包子铺有 种蒸笼,其中第 种蒸笼恰好能放 个包 子。每种蒸笼都有非常多笼,可以认为是无限笼。
每当有顾客想买X个包子,卖包子的大叔就会迅速选出若干笼包子来,使得这若干笼中恰好一共有 X个包子。比如一共有 3 种蒸笼,分别能放 3、4 和 5 个包子。当顾客想买 11 个包子时,大叔就会选 2 笼 3 个的再加 1 笼 5 个的(也可能选出 1 笼 3 个的再加 2 笼 4 个的)。
当然有时包子大叔无论如何也凑不出顾客想买的数量。比如一共有 3 种蒸笼,分别能放 4、5 和 6 个包子。而顾客想买 7 个包子时,大叔就凑不出来了。
小明想知道一共有多少种数目是包子大叔凑不出来的。
输入描述
第一行包含一个整数N (1 <= N <= 100 )。 以下 N 行每行包含一个整数 (1 <=
<= 100 )。
输出描述
一个整数代表答案。如果凑不出的数目有无限多个,输出 INF。
思路
我们先分析清楚什么样的情况下凑不出来的个数为INF个?
首先我们可以先对这个题目进行一步抽象。
先明确,
,……,
为每笼包子的个数,
,
,……,
为有多少笼包子。C为我们需要凑出的包子的个数,则有下列一个等式。
+
+
+ …… +
= C
如果给定数列{,
,
,……,
}的最大公约数d不为1,因为
+
+
+ …… +
一定能整除d。所以,所有不能整除d的数都不能被给定数列表示出来,因此无法凑出来的数就有INF个。
什么情况下凑不出来的个数为有限个?
如果给定数列{,
,
,……,
}的最大公约数d为1,即两两互质,则根据“裴蜀定理”,一定存在非零整数
,
,……,
,使得
+
+
+ …… +
= 1。
我们再等式左右两边乘上任意数C等式仍相等,所以理论上可以凑出任意数。
即,如果(,
,
,……,
)互质,则
+
+
+ …… +
= C一定有解。
若加上限制条件:,
,……,
>= 0,则使
+
+
+ …… +
= C无解的C的个数有限。
问题转换
若(,
,
,……,
)互质,问题转换为“完全背包”问题。
![](https://i-blog.csdnimg.cn/blog_migrate/fa6807363789c552493ee00fa16b3189.png)
状态转移方程:f[i][j] = f[i - 1][j] | f[i][j - a[i]]
我们究竟需要枚举到多少个包子,才能确保我们所有不能被凑出来的包子全部都被囊括进来了。即j的上限是多少?
定理:如果p,q均是正整数且互质,那么px + qy(x >= 0, y >= 0)不能表示的最大的数为pq - p - q = (p - 1)(q - 1) - 1。
由于两个互质的数不能凑出的最大数为(a - 1)*(b - 1) - 1。
则多个数去凑,不能凑出的数的最大值一定<= (a - 1)*(b - 1) - 1。
所以,若(,
,
,……,
)互质,根据题意,1 <=
<= 100,则凑不出的数的上界是:100 * 100 - 100 - 100。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110, M = 10010;
int n;
int a[N];
bool f[N][M];
int gcd (int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int main()
{
scanf("%d", &n);
int d = 0;
for(int i = 1; i <= n; i ++ )
{
scanf("%d", &a[i]);
d = gcd(d, a[i]);
}
if(d != 1) puts("INF");
else
{
f[0][0] = true; //赋初值
for(int i = 1; i <= n; i ++ )
for(int j = 0; j <= M; j ++ )
{
f[i][j] = f[i - 1][j];
if(j >= a[i]) f[i][j] |= f[i][j - a[i]];
}
int res = 0;
for(int i = 0; i < M; i ++ )
if(!f[n][i]) res ++;
cout << res << endl;
}
}
六、地宫取宝(DP,2014)
题目描述
X 国王有一个地宫宝库。是 n x m个格子的矩阵。每个格子放一件宝贝。每个宝贝贴着价值标签。地宫的入口在左上角,出口在右下角。
小明被带到地宫的入口,国王要求他只能向右或向下行走。
走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
当小明走到出口时,如果他手中的宝贝恰好是 k件,则这些宝贝就可以送给小明。
请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这 k件宝贝。
输入描述
输入一行 3 个整数,用空格分开:n, m, k(1 <= n, m <= 50, 1 <= k <= 12) 。 接下来有 n行数据,每行有 m个整数 (0 <=
<= 12)代表这个格子上的宝物的价值。
输出描述
要求输出一个整数,表示正好取 k个宝贝的行动方案数。该数字可能很大,输出它对 + 7取模的结果。
思路
题目的限制条件:
1)只能向右和向下走
2)只能按严格单调递增的顺序取物品
3)一共恰好取k件
因此我们的状态表示有四维,后两维分别代表最后一次取的物品的价值和已经取了多少件物品。
![](https://i-blog.csdnimg.cn/blog_migrate/b820b4b8be87cd38df607ae23d9ea65b.png)
初始状态
f[1, 1, 1, w(1, 1)] = 1 —— 走到(1,1)取物品
f[1, 1, 0, -1] = 1 —— 走到(1,1)不取
最后一件物品不存在,其价值只能用一个小于所有价值的值-1表示。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 55, mod = 1e9 + 7;
int n, m, k;
int w[N][N];
int f[N][N][14][14];
int main()
{
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
{
scanf("%d", &w[i][j]);
w[i][j] ++;
}
f[1][1][1][w[1][1]] = 1;
f[1][1][0][0] = 1;
/*初始化时,第一件不取时“最后一件的物品的价值”不存在,只能取小于所有价值的-1,
所以v的取值范围为:-1 <= v <= 12。但是数组没有-1的下标,所以将所有v都加1, v的取值范围变为
0 <= v <= 13。同时,f[1][1][0][-1] = 1变为f[1][1][0][0] = 1*/
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
{
if(i == 1 && j == 1) continue;
for(int u = 0; u <= k; u ++ )
for(int v = 0; v <= 13; v ++ )
{
int &val = f[i][j][u][v]; //这个引用的目的是传值,因为f[i][j][u][v]太长了,写起来不方便
val = (val + f[i - 1][j][u][v]) % mod;
val = (val + f[i][j - 1][u][v]) % mod;
if(u > 0 && v == w[i][j])
{
for(int c = 0; c < v; c ++ )
{
val = (val + f[i - 1][j][u - 1][c]) % mod;
val = (val + f[i][j - 1][u - 1][c]) % mod;
}
}
}
}
int res = 0;
for(int i = 0; i <= 13; i ++ ) res = (res + f[n][m][k][i]) % mod;
cout << res << endl;
return 0;
}