在考场中,时常会遇到题目拿到没想法,就先来打个表或写个暴力来骗分。DFS就是一个骗分神器。如果能加上几道优化,或许会有意想不到的结果。
例1:数的划分
类型 DFS+枚举优化
题目
题解
$ 这题在爆搜的时候,可以给后面留下足够的空间。$
$ 什么意思呢?假设当前的值为 x,选择 x 之后要再选\ k-1 个,那么要保证\ yu \geq k*x,yu表示n剩下的部分,$
$ 因为后面还要选择k-1个大小至少为x的数字。$
代码
#include<bits/stdc++.h>
using namespace std;
int n,m,ans;
void dfs(int sum,int lst,int k)
{
if (k==1){++ans;return;}
for (int i=lst;i*k<=sum;++i) dfs(sum-i,i,k-1);
}
int main()
{
scanf("%d%d",&n,&m);
dfs(n,1,m);printf("%d\n",ans);
return 0;
}
例2:蛋糕
类型 DFS+预判
题目
题解
这 题 看 似 D F S 不 可 做 , 实 际 上 加 入 大 量 预 判 剪 枝 后 , 跑 得 飞 快 。 这题看似DFS不可做,实际上加入大量预判剪枝后,跑得飞快。 这题看似DFS不可做,实际上加入大量预判剪枝后,跑得飞快。
先 预 判 体 积 。 如 果 发 现 接 下 来 按 照 最 小 的 方 式 放 都 不 能 使 得 体 积 小 于 n 或 是 按 照 最 大 的 方 式 放 都 不 能 达 到 n 就 停 先预判体积。如果发现接下来按照最小的方式放都不能使得体积小于 \ n\ 或是按照最大的方式放都不能达到\ n\ 就停 先预判体积。如果发现接下来按照最小的方式放都不能使得体积小于 n 或是按照最大的方式放都不能达到 n 就停
再 判 表 面 积 。 如 果 接 下 来 拿 到 最 小 的 表 面 积 都 没 比 当 前 a n s 更 优 秀 , 那 么 就 停 。 或 者 可 以 用 另 一 种 ( 详 见 代 码 ) 再判表面积。如果接下来拿到最小的表面积都没比当前\ ans\ 更优秀,那么就停。或者可以用另一种(详见代码) 再判表面积。如果接下来拿到最小的表面积都没比当前 ans 更优秀,那么就停。或者可以用另一种(详见代码)
还
有
就
是
枚
举
的
时
候
可
以
预
判
r
和
h
的
取
值
范
围
。
还有就是枚举的时候可以预判\ r\ 和\ h\ 的取值范围。
还有就是枚举的时候可以预判 r 和 h 的取值范围。
上
界
:
r
m
a
x
=
min
{
y
u
,
上
次
选
的
r
}
,
h
m
a
x
=
min
{
y
u
/
r
,
上
次
选
的
h
}
上界:r_{max}=\min \{\sqrt {yu},上次选的\ r\},h_{max}=\min \{yu/r,上次选的\ h\}
上界:rmax=min{yu,上次选的 r},hmax=min{yu/r,上次选的 h}
下
界
:
r
m
i
n
=
h
m
i
n
=
min
{
1
,
h
i
}
下界:r_min=h_min=\min\{1,hi\}
下界:rmin=hmin=min{1,hi}
其
中
y
u
表
示
剩
余
的
体
积
,
h
i
表
示
剩
余
的
层
数
。
其中\ yu\ 表示剩余的体积,\ hi\ 表示剩余的层数。
其中 yu 表示剩余的体积, hi 表示剩余的层数。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int n,m;LL ans=1e18,f1[25],f2[25];
void dfs(LL hi,LL yu,LL R,LL H,LL S)
{
bool pd=hi==m;
if (hi==1)
{
for (LL r=min(R,(LL)sqrt(yu));r>0;--r)
{
LL p=pd?r*r:0;LL h=yu/r/r;if (h>H) break;
if (h*r*r==yu) ans=min(ans,S+p+(h*r<<1));
}
return;
}
if (yu>hi*H*R*R||yu<f1[hi]||S+max(yu/R<<1,f2[hi])>ans) return;
/*如果接下来hi层都按照最大都放不满yu单位空间*/
/*如果接下来按照最小方式放蛋糕体积都超过n*/
/*f2是最少要得到的侧面积。对于一个蛋糕,不考虑上下表面,那么最小的方式肯定要使得r尽可能大
当前情况下,r最大取到R。假设后面层都取到R,那么几层可以看成一层,侧面积为yu/R*2 */
for (LL r=min(R,(LL)sqrt(yu));r>=hi;--r)
{
LL p=pd?r*r:0;
for (LL h=min(H,yu/(r*r));h>=hi;--h)
{
dfs(hi-1,yu-r*r*h,r-1,h-1,S+(h*r<<1)+p);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;++i) f1[i]=f1[i-1]+i*i*i,f2[i]=f2[i-1]+i*i*2;
dfs(m,n,sqrt(n),n,0);
printf("%lld",ans==1e18?0:ans);
return 0;
}
例3:小木棍
类型 DFS+预判+枚举优化
题目
题解
这
题
乍
一
看
还
以
为
是
贪
心
,
实
际
上
是
有
漏
洞
的
。
没
办
法
,
只
能
写
D
F
S
。
这题乍一看还以为是贪心,实际上是有漏洞的。没办法,只能写DFS。
这题乍一看还以为是贪心,实际上是有漏洞的。没办法,只能写DFS。
但
是
n
=
60
还
是
比
较
大
的
,
想
想
办
法
尽
可
能
优
化
一
下
。
但是n=60还是比较大的,想想办法尽可能优化一下。
但是n=60还是比较大的,想想办法尽可能优化一下。
枚举方式
如
果
懒
得
写
链
表
,
我
们
可
以
枚
举
长
度
,
而
不
是
枚
举
木
棍
(
长
度
小
于
数
量
)
如果懒得写链表,我们可以枚举长度,而不是枚举木棍(长度小于数量)
如果懒得写链表,我们可以枚举长度,而不是枚举木棍(长度小于数量)
枚举方式
从
m
i
n
(
上
次
选
择
长
度
,
最
长
的
长
度
,
这
根
剩
余
的
长
度
)
开
始
枚
举
。
从min(上次选择长度,最长的长度,这根剩余的长度)开始枚举。
从min(上次选择长度,最长的长度,这根剩余的长度)开始枚举。
预判
如果当前这根剩下的部分小于有最短的木棍可以直接弹出。
枚举方式
枚
举
的
时
候
,
按
照
从
大
到
小
的
顺
序
枚举的时候,按照从大到小的顺序
枚举的时候,按照从大到小的顺序
如
果
所
剩
的
木
棍
可
以
凑
到
长
度
L
e
n
,
那
么
跟
顺
序
肯
定
没
关
系
如果所剩的木棍可以凑到长度Len,那么跟顺序肯定没关系
如果所剩的木棍可以凑到长度Len,那么跟顺序肯定没关系
但
是
按
顺
序
可
以
避
免
重
复
但是按顺序可以避免重复
但是按顺序可以避免重复
预判
假
设
我
们
上
一
次
刚
好
凑
出
一
根
木
棍
,
剩
余
木
棍
的
集
合
S
,
找
到
了
最
长
的
木
棍
x
,
且
x
∈
S
。
假设我们上一次刚好凑出一根木棍,剩余木棍的集合S,找到了最长的木棍 x,且\ x\in S。
假设我们上一次刚好凑出一根木棍,剩余木棍的集合S,找到了最长的木棍x,且 x∈S。
而
选
择
这
根
木
棍
后
接
下
来
却
凑
不
出
一
根
木
棍
,
那
么
就
直
接
弹
出
。
而选择这根木棍后接下来却凑不出一根木棍,那么就直接弹出。
而选择这根木棍后接下来却凑不出一根木棍,那么就直接弹出。
剩
余
木
棍
的
肯
定
不
能
和
x
凑
成
新
木
棍
。
剩余木棍的肯定不能和\ x\ 凑成新木棍。
剩余木棍的肯定不能和 x 凑成新木棍。
即
使
换
一
根
短
一
点
的
木
棍
可
以
凑
出
来
整
根
,
凑
出
后
x
肯
定
没
被
使
用
过
,
否
则
刚
才
就
用
了
。
即使换一根短一点的木棍可以凑出来整根,凑出后\ x\ 肯定没被使用过,否则刚才就用了。
即使换一根短一点的木棍可以凑出来整根,凑出后 x 肯定没被使用过,否则刚才就用了。
x
和
刚
才
那
个
集
合
S
中
的
就
不
能
凑
成
了
,
现
在
的
集
合
比
S
还
要
小
,
更
不
可
能
凑
出
整
根
了
。
x \ 和刚才那个集合 S 中的就不能凑成了,现在的集合比 S 还要小,更不可能凑出整根了。
x 和刚才那个集合S中的就不能凑成了,现在的集合比S还要小,更不可能凑出整根了。
后 面 两 个 的 效 果 比 较 明 显 。 后面两个的效果比较明显。 后面两个的效果比较明显。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=65;
int n,a[maxn],h[55],p[55],MX,MI,sum,Len,LON;
void dfs(int yu,int stp,int lst)
{
if (stp==n) {printf("%d",Len);exit(0);};
bool pd;if (pd=!yu) lst=min(yu=Len,MX);
for (int i=lst;i>=MI;++h[i--])
if (h[i]--&&(dfs(yu-i,stp+1,min(i,yu-i)),true)&&pd) {++h[i];return;}
}
int main()
{
scanf("%d",&n);MI=1e9;
for (int i=1;i<=n;++i) scanf("%d",&a[i]),++p[a[i]],sum+=a[i],MX=max(MX,a[i]),MI=min(MI,a[i]);LON=sum>>1;
for (Len=MX,memcpy(h,p,sizeof p);Len<=LON;++Len) if (sum%Len==0) {dfs(0,0,0);memcpy(h,p,sizeof p);}
printf("%d\n",sum);return 0;
}
例4:Addition Chains
类型 DFS+预判
题目
题解
这
题
在
搜
索
过
程
中
,
我
们
可
以
提
前
预
判
,
如
果
当
前
长
度
l
e
n
+
g
e
t
(
a
[
l
e
n
]
)
≥
当
前
最
优
解
这题在搜索过程中,我们可以提前预判,如果\ 当前长度len\ +\ get(a[len]) \geq \ 当前最优解
这题在搜索过程中,我们可以提前预判,如果 当前长度len + get(a[len])≥ 当前最优解
其
中
,
g
e
t
(
a
[
l
e
n
]
)
为
一
个
估
价
函
数
,
表
示
当
前
最
大
值
为
a
[
l
e
n
]
的
情
况
下
,
只
是
还
需
几
步
使
得
a
[
l
e
n
]
=
n
其中,get(a[len]) \ 为一个估价函数,表示当前最大值为\ a[len]\ 的情况下,只是还需几步使得\ a[len] = n
其中,get(a[len]) 为一个估价函数,表示当前最大值为 a[len] 的情况下,只是还需几步使得 a[len]=n
a
[
l
e
n
]
的
增
长
速
度
最
快
是
每
次
扩
大
2
倍
。
假
如
有
一
个
最
小
的
k
满
足
2
k
+
a
[
l
e
n
]
≥
n
,
这
个
k
就
是
g
e
t
(
a
[
l
e
n
]
)
a[len]\ 的增长速度最快是每次扩大\ 2\ 倍。假如有一个最小的\ k \ 满足\ 2^k+a[len] \geq n,这个\ k\ 就是\ get(a[len])
a[len] 的增长速度最快是每次扩大 2 倍。假如有一个最小的 k 满足 2k+a[len]≥n,这个 k 就是 get(a[len])
如
果
a
[
l
e
n
]
连
达
到
n
都
做
不
到
,
那
么
就
更
别
提
与
n
相
等
了
。
如果\ a[len] 连达到\ n\ 都做不到,那么就更别提与\ n\ 相等了。
如果 a[len]连达到 n 都做不到,那么就更别提与 n 相等了。
其
次
还
可
以
在
枚
举
的
时
候
加
上
一
些
优
化
,
比
如
:
因
为
我
们
的
a
序
列
是
递
增
的
,
而
且
新
的
元
素
要
大
于
当
前
序
列
最
大
值
其次还可以在枚举的时候加上一些优化,比如:因为我们的\ a\ 序列是递增的,而且新的元素要大于当前序列最大值
其次还可以在枚举的时候加上一些优化,比如:因为我们的 a 序列是递增的,而且新的元素要大于当前序列最大值
所
以
我
们
可
以
倒
着
枚
举
i
和
j
,
这
样
一
旦
发
现
a
[
i
]
+
a
[
j
]
<
=
a
[
l
e
n
]
那
么
就
可
以
b
r
e
a
k
了
,
接
下
来
j
是
递
减
的
,
a
[
i
]
+
a
[
j
′
]
一
定
小
于
a
[
l
e
n
]
所以我们可以倒着枚举\ i\ 和\ j\ ,这样一旦发现\ a[i]+a[j]<=a[len]\ 那么就可以break了,接下来\ j\ 是递减的,a[i]+a[j']一定小于a[len]
所以我们可以倒着枚举 i 和 j ,这样一旦发现 a[i]+a[j]<=a[len] 那么就可以break了,接下来 j 是递减的,a[i]+a[j′]一定小于a[len]
代码
#include<bits/stdc++.h>
using namespace std;
int n,a[105],ans,b[105];
int get(int x){int s=0;while(x<n) ++s,x<<=1;return s;}
void dfs(int stp)
{
if (stp+get(a[stp])>=ans) return;
if (a[stp]==n) {ans=stp;for (int k=1;k<=stp;++k)b[k]=a[k];return;}
for (int i=stp;i>=1;--i)
for (int j=stp;j>=i;--j)
if (a[i]+a[j]>a[stp])
{
if (a[i]+a[j]>n) continue;
a[stp+1]=a[i]+a[j];dfs(stp+1);
}else break;
}
int main()
{
a[1]=1;
while(true)
{
scanf("%d",&n);
if (! n) break;
ans=1e9;
dfs(1);
for (int i=1;i<ans;++i)printf("%d ",b[i]);printf("%d\n",b[ans]);
}
return 0;
}
例5:weight
类型 DFS+枚举剪枝(贪心)
题目
题解
这
题
首
先
想
到
的
是
枚
举
每
一
个
a
i
是
集
合
中
的
第
几
号
元
素
。
这题首先想到的是枚举每一个a_i是集合中的第几号元素。
这题首先想到的是枚举每一个ai是集合中的第几号元素。
这
样
加
上
提
前
判
断
能
否
满
足
前
缀
和
和
后
缀
和
,
也
是
要
超
时
的
。
这样加上提前判断能否满足前缀和和后缀和,也是要超时的。
这样加上提前判断能否满足前缀和和后缀和,也是要超时的。
突
发
奇
想
:
因
为
所
有
元
素
都
是
正
整
数
,
所
以
前
缀
和
肯
定
递
增
,
后
缀
和
肯
定
递
减
。
突发奇想:因为所有元素都是正整数,所以前缀和肯定递增,后缀和肯定递减。
突发奇想:因为所有元素都是正整数,所以前缀和肯定递增,后缀和肯定递减。
对
表
示
前
后
缀
和
的
序
列
S
排
序
,
对
于
最
小
的
一
个
前
缀
和
或
后
缀
和
,
它
不
是
第
一
个
元
素
,
就
是
最
后
一
个
元
素
。
对表示前后缀和的序列S排序,对于最小的一个前缀和或后缀和,它不是第一个元素,就是最后一个元素。
对表示前后缀和的序列S排序,对于最小的一个前缀和或后缀和,它不是第一个元素,就是最后一个元素。
1证明
对 于 第 二 个 元 素 也 是 差 不 多 的 道 理 。 剩 下 的 都 可 以 以 此 类 推 。 对于第二个元素也是差不多的道理。剩下的都可以以此类推。 对于第二个元素也是差不多的道理。剩下的都可以以此类推。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1005,maxm=505;
int n,m,h[maxm+5],S[maxn<<1],s1[maxn],s2[maxn],Sum,b[maxn];
bool pd(int x){return x>=1&&x<=500&&h[x];}
void dfs(int p,int L,int R)
{
if (L==R)
{
if (pd(Sum-s1[L-1]-s2[R+1]))
{
b[L]=Sum-s1[L-1]-s2[R+1];
for (int i=1;i<n;++i) printf("%d ",b[i]);
printf("%d\n",b[n]);exit(0);
}
return;
}
b[L]=S[p]-s1[L-1];
if (pd(b[L]))
{
s1[L]=S[p];
dfs(p+1,L+1,R);
s1[L]=0;
}
b[L]=0;
b[R]=S[p]-s2[R+1];
if (pd(b[R]))
{
s2[R]=S[p];
dfs(p+1,L,R-1);
s2[R]=0;
}
b[R]=0;
}
int main()
{
scanf("%d",&n);n<<=1;
for (int i=1;i<=n;++i) scanf("%d",&S[i]);sort(S+1,S+n+1);Sum=S[n];
scanf("%d",&m);n>>=1;
for (int x,i=1;i<=m;++i) scanf("%d",&x),h[x]=1;
dfs(1,1,n);
return 0;
}
练习1:埃及分数
类型 DFS+预判
题目
题解
之前写过了,直接套链接
代码
练习2:平板涂色
本蒟蒻的代码找不到问题,但是就是WA……
练习3:靶形数独
类型 DFS+枚举剪枝
题目
题解
这 题 在 0 的 个 数 比 较 多 的 时 候 可 能 会 超 时 。 这题在 0 的个数比较多的时候可能会超时。 这题在0的个数比较多的时候可能会超时。
但
是
经
过
观
察
,
我
们
可
以
先
枚
举
确
定
已
经
的
数
字
个
数
较
多
的
行
(
列
也
是
同
理
的
)
但是经过观察,我们可以先枚举确定已经的数字个数较多的行(列也是同理的)
但是经过观察,我们可以先枚举确定已经的数字个数较多的行(列也是同理的)
因
为
这
样
子
我
们
可
以
减
少
肯
定
不
必
要
的
枚
举
。
因为这样子我们可以减少肯定不必要的枚举。
因为这样子我们可以减少肯定不必要的枚举。
就
比
如
说
第
5
行
第
1
个
数
字
是
这
行
唯
一
一
个
空
位
,
必
定
填
3
。
若
完
全
按
照
顺
序
枚
举
,
那
么
第
1
、
2
、
3
行
的
第
1
个
位
置
都
可
能
被
枚
举
到
为
3
(
很
显
然
第
1
列
不
可
能
存
在
任
何
一
个
已
知
数
字
为
3
,
因
为
这
个
3
已
经
可
以
通
过
推
理
确
定
在
第
5
行
第
1
个
)
就比如说第 5 行第 1 个数字是这行唯一 一个空位,必定填 3。若完全按照顺序枚举,那么第 1、2、3 行的第 1 个位置都可能被枚举到为 3(很显然第 1 列不可能存在任何一个已知数字为 3,因为这个 3 已经可以通过推理确定在第 5 行第 1 个)
就比如说第5行第1个数字是这行唯一一个空位,必定填3。若完全按照顺序枚举,那么第1、2、3行的第1个位置都可能被枚举到为3(很显然第1列不可能存在任何一个已知数字为3,因为这个3已经可以通过推理确定在第5行第1个)
若
先
把
这
个
3
定
住
,
那
将
减
少
多
少
不
必
要
的
枚
举
啊
若先把这个 3 定住,那将减少多少不必要的枚举啊
若先把这个3定住,那将减少多少不必要的枚举啊
本
思
路
来
自
洛
谷
题
解
本思路来自洛谷题解
本思路来自洛谷题解
https://www.luogu.org/problemnew/solution/P1074
其
中
还
有
很
多
很
好
的
想
法
,
本
蒟
蒻
功
力
不
足
,
暂
且
就
说
这
些
吧
。
其中还有很多很好的想法,本蒟蒻功力不足,暂且就说这些吧。
其中还有很多很好的想法,本蒟蒻功力不足,暂且就说这些吧。
代码
#include<bits/stdc++.h>
using namespace std;
const int f[6]={0,6,7,8,9,10};
int a[15][15],x[15][15],y[15][15],t[15][15],id[15][15],h[7],hx[15],ans=-1;
int nxt()
{
int id=10;
for (int i=1;i<=9;++i) if (hx[i]<9&&hx[i]>hx[id]) id=i;
return id;
}
int g(int F)
{
int sum=0;
for (int i=1;i<=5;++i) sum+=h[i]*9*f[F];
return sum;
}
void dfs(int xx,int yy,int s)
{
if (yy>9) {xx=nxt();yy=1;}
int F=min(min(10-xx,10-yy),min(xx,yy));
if (ans>=s+g(F)) return;
if (xx>9) {ans=s;return;}
if (a[xx][yy]){dfs(xx,yy+1,s);return;}
for (int i=1;i<=9;++i)
{
if (!x[xx][i]&&!y[yy][i]&&!t[id[xx][yy]][i])
{
x[xx][i]=y[yy][i]=t[id[xx][yy]][i]=1;
a[xx][yy]=i;--h[F];++hx[xx];
dfs(xx,yy+1,s+i*f[F]);
x[xx][i]=y[yy][i]=t[id[xx][yy]][i]=0;
a[xx][yy]=0;++h[F];--hx[xx];
}
}
}
int main()
{
for (int k=0;k<3;++k)
for (int t=0;t<3;++t)
{
int m=k*3+t+1;
for (int i=1;i<=3;++i)
for (int j=1;j<=3;++j) id[i+k*3][j+t*3]=m;
}
int S=0;
h[1]=32;
h[2]=24;
h[3]=16;
h[4]=8;
h[5]=1;
for (int i=1;i<=9;++i)
for (int j=1;j<=9;++j)
{
scanf("%d",&a[i][j]);
t[id[i][j]][a[i][j]]=1;
x[i][a[i][j]]=1;
y[j][a[i][j]]=1;
int F=min(min(10-i,10-j),min(i,j));
if (a[i][j]) S+=a[i][j]*f[F],--h[F],++hx[i];
}
hx[10]=-1;
dfs(nxt()!=10?nxt():1,1,S);
printf("%d",ans);
return 0;
}
既然写证明,就写详细点吧我 们 先 对 一 开 始 的 S 序 列 排 序 我们先对一开始的S序列排序 我们先对一开始的S序列排序 ↩︎