爱情到来绝不能犹豫。
关注博主容不得迟缓。
§( ̄▽ ̄)§
试题:质数拆分
题目描述:
将2019 拆分为若干个两两不同的质数之和,一共有多少种不同的方法?
注意交换顺序视为同一种方法,例如 2+2017=2019 与 2017 + 2 = 2019视为同一种方法。
解析:
我们很容易想到,如果我们求2+2017=2019而2017又可以拆成X+X这样的形式,所以2019的方案数应该是“配对成2的个数”加“配对乘2017”所有的个数。
由此我们想到了dp的转移方案:
前i个数指的是:2,3,5,7,。。。这样的数(使用素数筛查先算出来这个序列)
dp[i][j]表示千i个数配出j的方案总数。
那么dp[i][j]由dp[i-1][j]转移一次,看看不选第i个,能不能配出。
然后再由dp[i-1][j-prime[i]]看看如果选了第i个,能不能配出。
正解代码:
#include <cstring>
#include <iostream>
using namespace std;
bool is_prime[2019 + 5];
#define N 2019
int prime[1000], p = 0; // p就是prime元素的个数
unsigned long long dp[1000][N + 5] = {0};
void do_prime(int x) { //埃氏素数筛法
memset(is_prime, true, sizeof(is_prime));
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= x; i++) {
if (is_prime[i]) {
prime[p++] = i;
for (int j = i * 2; j <= x; j += i) {
is_prime[j] = false;
}
}
}
}
int main() {
memset(prime, 0, sizeof(prime));
do_prime(N); //筛选素数
dp[0][0] = 1;
for (int i = 0; i < p; i++) {
for (int j = 0; j <= N; j++) {
dp[i + 1][j] += dp[i][j];
if (j - prime[i] >= 0)
dp[i + 1][j] += dp[i][j - prime[i]];
}
}
cout << dp[p][N];
return 0;
}
试题:拼接
题目描述:
小明要把一根木头切成两段,然后拼接成一个直角。
如下图所示,他把中间部分分成了n X n 的小正方形,他标记了每个小正方形属于左边还是右边。然后沿两边的分界线将木头切断,将右边旋转向上后拼 接在一起。

要求每个小正方形都正好属于左边或右边,而且同一边的必须是连通的。在拼接时,拼接的部位必须保持在原来大正方形里面。
请问,对于 7×7 的小正方形,有多少种合法的划分小正方形的方式。
解析:
首先非常有必要理解,这个右边这个方块是怎么跑到最上面的。
实际上,这个方块跑到最上面是因为,按照左下这个点和右上这个点的连线对折,然后去折叠上去的。
请看下图:

那么对于图中的样例:
我们可以模拟这个过程去找规律。

我们固定标有“左”这个字符的方块不动,然后对“标有右”这个字符的方块进行反转。
其中对于右边得这个方块,在对称线(最下角到右上角)的上方的会被反转到下方。而在对称线的下方则会反转到上方。
注意观察最后一幅图片的黑色线。反转之后,黑色线会重合。这是因为,切开的方块再拼的时候,缝隙要对齐。
**程序突破口:分割线一定会按照对称线对称。**也只有这样,割下来才可能进行一个完美的反转然后拼在一起。
设计:
我们可以在分割线以下的区域进行画线。从分割线出发,一直画线到末尾。计算总共我们能画出多少条线。(分割线以上不用画线的原因是,因为存在对称线,分割线以下画好,分割线已上就自然画好了)
坑点1:我们从对称线出发,不能再次回到对称线。

如上图,这样的话,会导致中间割出一个空洞。
坑点2:在边缘处要特别小心。
比如此图,右边到底链接的是(1)还是(2)。???或者说,X是掉下来的。
所以dfs当到最底下,不能再往上去搜索,注意观察X号左边(红色叉叉)的那一个边。是不能再次往上的。
还有几个坑点(1)画出的线不能交叉。(2)边界等等。
边界:我们总是想要让停止的地方在最右侧。
这是因为:
你看,现在没有在最右侧停止,然后现在最左上方的那个木块更本就没有割下来。
对于对称线的最左顶点(0,0)只有一种就是横着画。
同一最右顶点(7,7)也只有竖着画。总共多出2种答案。
#include <iostream>
using namespace std;
int ans = 0;
int vis[8][8];
int dx[] = {1, 0, -1, 0};
int dy[] = {0, -1, 0, 1};
void dfs(int x, int y) {
if (x == 7) {
ans++;
return;
}
vis[x][y] = 1;
for (int i = 0; i < 4; i++) {
int tx = x + dx[i], ty = y + dy[i];
if (y == 0 && ty == y + 1)//对于坑点:边界要特别小心
continue;
if ((vis[tx][ty] == 1) || (tx < 0 || ty < 0) || (tx <= ty))
//tx<=ty是说,判断我们有没有跑出去或者将要到分界线上。在y=x之下是y<x
continue;
dfs(tx, ty);
}
vis[x][y] = 0;
}
int main() {
for (int i = 1; i < 7; i++) {
dfs(i, i); //从对角线上每一个点出发查找
}
//别忘记加上最后(0,0)和(7,7)两个点的方案。每个点只有一种切法,所以+2
cout << ans + 2;
return 0;
}
试题:路径计数
题目描述:
解析:
建立(1,1)到(6,6)的坐标系。(1,1)到(1,2)是线段长度为1的线。
本题一看就知道是dfs,具体的dfs不在我们这篇文章讨论,因为比较基础。
本题技巧就是,怎么判断这个点是(1,1)的访问次数。也就是我们访问过(1,1)然后第二次访问。那么我们在进行下一层的深搜的时候人为加上条件(Tx,Ty)为(1,1)的时候继续搜。这样就可以手动强制再搜索一次。
(1,1)第二次访问之后还是要小心:出发之后立即就返回的情况,什么意思:

此时step=2(虽然被两次访问),但是我们还是要排除这种答案。当step>2的时候,我们才ans++。(请结合代码理解)
#include <iostream>
using namespace std;
int map[10][10];
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
int ans = 0;
bool check(int x, int y) {
return x >= 1 && x <= 6 && y >= 1 && y <= 6;
}
void dfs(int x, int y, int step) {
if (step > 12) { //没必要的访问
return;
}
if (map[x][y] == 1) { //这个点染过染色却还是被访问了,只有一种可能,这个点是(1,1)
// cout << x << " " << y << " " << step << endl;
if (step > 2) //考虑不利己返回。如(1,2,1)然后(1,1,2)这就是立即返回了,所以是step>2
ans++;
return;
}
map[x][y] = 1;
for (int i = 0; i < 4; i++) {
int tx = x + dx[i], ty = y + dy[i];
if ((tx == 1 && ty == 1) || (check(tx, ty) && map[tx][ty] == 0)) { //扩展的条件是没被染色,或者是(1,1)
dfs(tx, ty, step + 1);
}
}
map[x][y] = 0;
}
int main() {
dfs(1, 1, 0);
cout << ans;
return 0;
}
本题结束了吗?
还没有呢~我想让你观察下面的代码,然后告诉我为什么答案是208,不是206。
一个小小的提示:答案出错的原因在于step=4的时候出的错,step=4本应该有两种,可是错误答案有4种。
代码如下:
#include <iostream>
using namespace std;
int ans = 0;
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int vis[10][10];
//判断是否合法
bool check(int x, int y) {
return x >= 0 && x <= 5 && y >= 0 && y <= 5;
}
void dfs(int x, int y, int dep) {
if (dep > 4) { // dep>12 改为dep>4方便调试
return; //超过限制条件
}
if (x == 0 && y == 0 && dep > 2) {
//达成目标计数,dep>2保证原点出发不立刻返回
ans++;
return;
}
vis[x][y] = 1; //标记当前结点,保证路线不自交
vis[0][0] = 0;
for (int i = 0; i < 4; i++) {
//从当前结点(x,y)出发四个方向继续dfs未走过的路径
int xx = x + dir[i][0];
int yy = y + dir[i][1];
if (check(xx, yy) && !vis[xx][yy]) {
dfs(xx, yy, dep + 1);
}
}
vis[x][y] = 0; //去除标记,可能会被别的路线访问
}
int main() {
dfs(0, 0, 0); // dfs
cout << ans << endl;
return 0;
}
如果你说使用debug调试,我想这也许太复杂了。我们只需要创建一个stack。模拟入栈出栈的这个过程就可以了。
例如:
Sta.push_back(P(xx, yy));
dfs(xx, yy, dep + 1);
Sta.pop_back();
Debug代码全貌:
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int ans = 0;
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int vis[10][10];
typedef pair<int, int> P;
vector<P> Sta;
//判断是否合法
bool check(int x, int y) {
return x >= 0 && x <= 1 && y >= 0 && y <= 1;
}
void dfs(int x, int y, int dep) {
if (dep > 4) { // dep>12 改为dep>4方便调试
return; //超过限制条件
}
if (x == 0 && y == 0 && dep > 2) {
//达成目标计数,dep>2保证原点出发不立刻返回
ans++;
for (auto ite = Sta.begin(); ite != Sta.end(); ite++) {
cout << "(" << (*ite).first << "," << (*ite).second << ")" << endl;
}
cout << endl;
return;
}
vis[x][y] = 1; //标记当前结点,保证路线不自交
vis[0][0] = 0;
for (int i = 0; i < 4; i++) {
//从当前结点(x,y)出发四个方向继续dfs未走过的路径
int xx = x + dir[i][0];
int yy = y + dir[i][1];
if (check(xx, yy) && !vis[xx][yy]) {
Sta.push_back(P(xx, yy));
dfs(xx, yy, dep + 1);
Sta.pop_back();
}
}
vis[x][y] = 0; //去除标记,可能会被别的路线访问
}
int main() {
Sta.push_back(P(0, 0));
dfs(0, 0, 0); // dfs
Sta.pop_back();
cout << ans << endl;
return 0;
}
输出结果:

出问题的两组是:第一组,和第四组。
因为它们含有两个(0,0)这是不正确的。
因为从第二个(0,0)不应该是扩展点。也就是我们看第一组
(0,0)(0,1)(0,0)—>(1,0)
第二个(0,0)是不能继续往下搜索的。应该及时的返回。

这是我的调试界面。注意左边的数据。
现在x,y是(0,0)而Sta是调试vector,现在我们调试的就是第一组数据。
然后光标指向了第30行。没有及时地退出,反而继续进行了dfs。这是错误的。
在30行之前加入代码:
if (dep != 0 && (x == 0 && y == 0)) { //防止dep<=2的时候弹不出去。
return;
}
dep可以很好的帮助我们判断是第几个(0,0)当dep=0,的时候是第一个(0,0)是不需要弹出的。
现在代码修改完毕。运行结果恢复正常。

我们回到原问题:
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int ans = 0;
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int vis[10][10];
typedef pair<int, int> P;
vector<P> Sta;
//判断是否合法
bool check(int x, int y) {
return x >= 0 && x <= 5 && y >= 0 && y <= 5;
}
void dfs(int x, int y, int dep) {
if (dep > 12) { // dep>12 改为dep>4方便调试
return; //超过限制条件
}
if (x == 0 && y == 0 && dep > 2) {
//达成目标计数,dep>2保证原点出发不立刻返回
ans++;
/* 关闭调试输出
for (auto ite = Sta.begin(); ite != Sta.end(); ite++) {
cout << "(" << (*ite).first << "," << (*ite).second << ")" << endl;
}
cout << endl;
return;
*/
}
if (dep != 0 && (x == 0 && y == 0)) { //防止dep<=2的时候弹不出去。
return;
}
vis[x][y] = 1; //标记当前结点,保证路线不自交
vis[0][0] = 0;
for (int i = 0; i < 4; i++) {
//从当前结点(x,y)出发四个方向继续dfs未走过的路径
int xx = x + dir[i][0];
int yy = y + dir[i][1];
if (check(xx, yy) && !vis[xx][yy]) {
Sta.push_back(P(xx, yy));
dfs(xx, yy, dep + 1);
Sta.pop_back();
}
}
vis[x][y] = 0; //去除标记,可能会被别的路线访问
}
int main() {
Sta.push_back(P(0, 0));
dfs(0, 0, 0); // dfs
Sta.pop_back();
cout << ans << endl;
return 0;
}
现在答案恢复正常了,最终答案是206,我为什么花这么长的篇幅写这道简单问题的答案。因为这个问题,实际看起来简单。确实最容易出错的那种类型。很多很多的边界条件思考不到,各种小小的错误都可能导致最后答案的错误,所以一定不能放过细节上的问题,勤加思考。

4814

被折叠的 条评论
为什么被折叠?



