题目描述
农夫约翰打算建立一个栅栏将他的牧场给围起来,因此他需要一些特定规格的木材。于是农夫约翰到木材店购买木材。可是木材店老板说他这里只剩下少部分大规格的木板了。不过约翰可以购买这些木板,然后切割成他所需要的规格。而且约翰有一把神奇的锯子,用它来锯木板,不会产生任何损失,也就是说长度为 1010 的木板可以切成长度为 88 和 22 的两个木板。
你的任务:给你约翰所需要的木板的规格,还有木材店老板能够给出的木材的规格,求约翰最多能够得到多少他所需要的木板。
输入格式
第一行为整数 �(�≤50)m(m≤50) 表示木材店老板可以提供多少块木材给约翰。紧跟着 �m 行为老板提供的每一块木板的长度。
接下来一行(即第 �+2m+2 行)为整数 �(�≤1000)n(n≤1000),表示约翰需要多少木材。
接下来 �n 行表示他所需要的每一块木板的长度。木材的规格小于 3276732767。(对于店老板提供的和约翰需要的每块木板,你只能使用一次)。
输出格式
只有一行,为约翰最多能够得到的符合条件的木板的个数。
输入输出样例
输入 #1
4 30 40 50 25 10 15 16 17 18 19 20 21 25 24 30
输出 #1
7
输入 #2
3 20 10 10 9 3 3 3 5 5 7 8 8 9
输出 #2
7
思路
这个题,洛谷给打的标签是搜索和剪枝
那么,我们从搜索的角度考虑
可以固定符合条件的木板数量,枚举木材店的木材,判断能否满足固定的木板数量
整体思路:二分答案+剪枝dfs
二分答案
首先考虑二分答案的部分,我们要二分的是符合条件的木板数量
下界:0(木材店无法满足约翰的要求)
上界:n(约翰需要的木材数量)
l=0, r=n;
while (l<=r){
mid = (l+r)>>1;
if (dfs(mid)){
l = mid+1;
}
else r = mid-1;
}
printf("%d\n", r);//输出答案
dfs
我们贪心地考虑一下,如果我们满足约翰的mid个木材,那么我们要求这mid根木材尽可能小
所以,我们给约翰要求的木材长度升序排列,判断排序后约翰要求的前mid根木材能否被满足
代码中,我们用need数组表示约翰要求的木材长度,wood数组表示木材店可以提供的木材长度
用now来表示我们当前正要满足的约翰要求的木材(now的初值为mid),枚举wood数组中可以满足约翰要求的木材
如果可以满足约翰的需求(wood[i]>=need[now]),我们将wood[i]-need[now],紧接着将now--,考虑下一根约翰要求的木材
边界条件:
如果now==0,约翰的需求已经被满足了,我们return true
这样绝对是TLE的,我们考虑优化dfs
剪枝
1、可行性剪枝:
如果木材店剩余的木材都无法满足约翰的mid根木材的需求,直接return
如何判断?
对木材店的木材总量求和,存入变量sum
给dfs加一个参数left,保存多余的木材长度,当有单根长度小于每一根约翰要求的木材长度的木材店木材时,多余的木材长度要增加这根没用的木材的长度
那么,木材店剩余的木材 = 木材店木材总量-多余的木材长度
对排序后约翰要求的木材(need数组)做一个前缀和,存入数组s,如果s[mid]>木材店剩余木材长度,return false
if (sum-left<s[mid]) return 0;
int nxtLeft = left;
if (wood[i]<need[1]) nxtLeft += wood[i];
在二分答案之前,我们同样可以用这种方法来缩短右端点
int useless=0;//多余的木材长度,意义同left变量
for (int i=1; i<=m; ++i){
if (wood[i]<need[1]) useless += wood[i];
}
for (int i=1; i<=n; ++i) s[i] = s[i-1]+need[i];//前缀和
l=0, r=n;
while (s[r]>sum-useless && r) --r;//如果木材店剩余的木材都无法满足约翰的前r根木材的需求
2、去重(这个不加也可以AC,但是如果出现大量重复的数据,不加会很慢)
定义l为下一次枚举的木材店木材长度的左端点,并作为dfs的参数,出现重复的时候,将l定义为当前枚举的wood数组的元素的下标,下一次深搜时直接从l开始枚举,如果没有重复的时候,l=1
bool dfs(int now, int l, int left)
//用now来表示我们当前正要满足的约翰要求的木材
//l为下一次枚举wood数组的下表左端点
//left为多余的木材长度
{
if (!now) return 1;//mid根木材可以被满足
//可行性剪枝1
//剩余木材长度小于约翰要求的mid根木材的长度,return
if (sum-left<s[mid]) return 0;
bool flag=0;
for (int i=l; i<=m; ++i){
if (wood[i]>=need[now]){//当前木材店的木材可以满足约翰第now根木材
wood[i] -= need[now];//锯掉约翰要求的木材
int nxtLeft = left;
if (wood[i]<need[1]) nxtLeft += wood[i];
//当有单根长度小于每一根约翰要求的木材长度的木材店木材时,多余的木材长度要增加这根没用的木材的长度
if (need[now-1]==need[now]) flag = dfs(now-1, i, nxtLeft);//去重
else flag = dfs(now-1, 1, nxtLeft);
//回溯
wood[i] += need[now];
if (flag) return flag;
}
}
return 0;
}
完整代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int M = 55;
const int N = 1e3+5;
int m, n, l, r, mid, sum;//sum为木材店可提供的木材总长
int wood[M], need[N], s[N];
//wood存储木材店提供的木材长度,need存储约翰需要的木材长度,s是排序后need数组的前缀和
inline int read()//快读
{
int x=0, f=1; char c=getchar();
while (!(c>='0' && c<='9')){if (c=='-') f=-1; c=getchar();}
while (c>='0' && c<='9'){x=(x<<3)+(x<<1)+c-48; c=getchar();}
return f*x;
}
bool dfs(int now, int l, int left)
//用now来表示我们当前正要满足的约翰要求的木材
//l为下一次枚举wood数组的下表左端点
//left为多余的木材长度
{
if (!now) return 1;//mid根木材可以被满足
//可行性剪枝1
//剩余木材长度小于约翰要求的mid根木材的长度,return
if (sum-left<s[mid]) return 0;
bool flag=0;
for (int i=l; i<=m; ++i){
if (wood[i]>=need[now]){//当前木材店的木材可以满足约翰第now根木材
wood[i] -= need[now];//锯掉约翰要求的木材
int nxtLeft = left;
if (wood[i]<need[1]) nxtLeft += wood[i];
//当有单根长度小于每一根约翰要求的木材长度的木材店木材时,多余的木材长度要增加这根没用的木材的长度
if (need[now-1]==need[now]) flag = dfs(now-1, i, nxtLeft);//去重
else flag = dfs(now-1, 1, nxtLeft);
//回溯
wood[i] += need[now];
if (flag) return flag;
}
}
return 0;
}
int main()
{
m=read();
for (int i=1; i<=m; ++i){
wood[i] = read();
sum += wood[i];
}
n=read();
for (int i=1; i<=n; ++i) need[i] = read();
sort(need+1, need+n+1);//need数组要升序排列
int useless=0;//多余的木材长度,意义同left变量
for (int i=1; i<=m; ++i){
if (wood[i]<need[1]) useless += wood[i];
}
for (int i=1; i<=n; ++i) s[i] = s[i-1]+need[i];//前缀和
l=0, r=n;
while (s[r]>sum-useless && r) --r;//如果木材店剩余的木材都无法满足约翰的前r根木材的需求
while (l<=r){//二分答案
mid = (l+r)>>1;
if (dfs(mid, 1, 0)){
l = mid+1;
}
else r=mid-1;
}
printf("%d\n", r);
return 0;
}
补充
上述代码评测结果
当不去重时,我们可以AC,但是有一个测试点明显慢了很多
当不用可行性优化时,我们会TLE