这几天参加夏令营,会不定时写几篇总结,嗯,就这。
至于为什么是从DAY3开始,那是因为前两天太水了
今天做了一些题目,还是有一些思维难度的。
那么现在开始,掌声!
1、小明的书包(bigbag)
题意描述:
小明有一个很大的书包,容量为c。但大家也知道,现在的课本也很重,以至于小明没有办法一次性带上所有的课本。小明把课本的重量和重要性告诉你,请你帮他算算选择哪些课本能使得重要性之和最大。
输入格式:
bigbag.in
第一行有两个正整数n和c,代表课本的数量和书包的容量。
接下来n行,每行有2个正整数wi和vi,分别代表该课本的重量和重要性。
输出格式:
bigbag.out
一行一个数,输出最大可能的重要性之和。
输入样例1
5 12
10 4
2 1
9 5
4 2
5 4
输出样例1
7
输入样例2
5 120000000
100000000 4
20000000 1
90000000 5
40000000 2
50000000 4
输出样例2
7
数据范围
对于 60%的数据,背包总容量和课本重量<=100
对于100%的数据,n <= 100,课本重要性<=100,背包总容量和课本重量<=10^9
分析
从题意可以看出:这其实是一道经典的01背包。但要注意:
背包总容量和课本重量<=10^9
如果按正常01背包来做的话,就会有如下代码:
for(int i=1;i<=n;i++)
for(int j=c;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
但是,这是按重量来枚举,而在这个题目中,背包容积最大是10的9次方。
如果我们开一个 这么大的数组,显然是会MLE的,这种方法不可行。
那么又应该怎样优化呢?
我们不妨这样想:我们用 来表示价值为 时的最小重量。
那么此时, 的值其实就是 (其中 含义如上所示, 为第 本书的重要度, 即为第 本书的重量)
这其实也就是我们的动态转移方程。
那么就可以得到如下代码:
Code
#include<bits/stdc++.h>
using namespace std;
int n;
int c;
int w[105],v[105];
long long f[100005]; //十年OI一场空,不开long long见祖宗
int sum;
int main(){
freopen("bigbag.in","r",stdin);
freopen("bigbag.out","w",stdout); //freopen输入输出,比赛经常用,不用管
scanf("%d%d",&n,&c);
for(int i=1;i<=n;i++){scanf("%d%d",&w[i],&v[i]);sum+=v[i];}
for(int i=1;i<=sum;i++) f[i]=INT_MAX; //因为求最小,所以要初始化为很大
for(int i=1;i<=n;i++)
for(int j=sum;j>=v[i];j--)
f[j]=min(f[j],f[j-v[i]]+w[i]); //动态转移方程,再次提醒:求最小
for(int i=sum;i>=0;i--) //从后枚举,保证先找到最大价值
if(f[i]<=c){printf("%d",i);return 0;} //判断,如果这个价值的最小重量比c大,那显然是不可能装下的,如果找到了小于c的f[i],这个i必然是最优解;
return 0;
}
其实挺水的对吧
2、邮票(post)
题意描述
市面上总共有N张不同的邮票,编号0至N-1。第i张邮票的价格是p[i]。
你的目标是能收集尽量多的不同邮票。如果你有足够多的钱,那显然不是问题,全部买回来就行了。
但是你手头上一分钱也没有,幸运的是一开始你手头上已经有m张邮票了,这些邮票的编号保存在数组b[i],0<=i<m。于是你决定通过卖掉你手头上的某些邮票,再买进一些其他的邮票,通过这样卖出买进,你最后最多可以有几张邮票(当然你也可以不做任何买卖)?
注意:对于任意的0<=j<N, 如果你卖出邮票j,那么你可以得到p[j]金钱,如果你想买进邮票j,那么你得付p[j]金钱。
输入格式
post.in
第一行,一个整数N,表示有N张邮票。1<=N<=50。
第二行, N个整数,第i个整数表示p[i],1<=p[i]<=1000000。
第三行,一个整数m,表示你现在已经有了m张邮票。0<=m<=n。
第四行, m个整数,第i个整数表示b[i]。
输出格式
post.out
一个整数,通过买卖,你最多可以有几张邮票?
样例输入1
4
13 10 14 20
4
3 0 2 1
样例输出1
3
样例解释1
一开始你已经有第0、1、2、3张邮票了,也就是你已经拥有所有的邮票了,不需要做交易了。
样例输入2
5
4 13 9 1 5
3
1 3 2
样例输出2
4
样例解释2
你可以把第2张邮票卖掉,得到9元钱,再买进第0张邮票和第4张邮票。
分析
我们想想:与其算哪张可以换哪几张,不如将所有手上的邮票卖出去,再去买邮票,算出种数,因为这题邮票价值是固定的,并不像洛谷P5662一样是每天都会变的。
所以,上述方法是可行的。
那么,在全部邮票卖出去之后,又应该用怎样的策略来买邮票呢?
有两种方法:
- DP,即01背包
- 贪心
但是我们会发现,如果使用DP做这题,很可能会错(不要问我为什么,因为我错了)
那我们的正解便是贪心了
详细解说见代码。
Code
#include<bits/stdc++.h>
using namespace std;
int n,m;
int V;
int p[55];
int b[55];
int f[50000005];
int ans;
int main(){
freopen("post.in","r",stdin);
freopen("post.out","w",stdout);
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&p[i]);
scanf("%d",&m);
for(int i=0;i<m;i++){
scanf("%d",&b[i]); //输入
V+=p[b[i]]; //计算把现在手上邮票全部卖掉后的钱数
}
sort(p,p+n); //排序
for(int i=0;i<n;i++){
if(V<p[i]) {
break;}
V-=p[i];
ans++;
} //贪心算法
cout<<ans<<endl; //输出
return 0;
}
3、 握手(shake)
题意描述
有N个人(N是偶数,不超过50),坐在一个园桌旁开会,会后,他们决定握手,每个人必须且只能挑另一个人握手, 假设对于握手的两个人,我们画都一条直线,我们要求所有的直线不能有交点。有多少种不同握手方法?
(好神奇的握手方式)
输入格式
ekg.in
一个整数N。N是偶数,不超过50。
输出格式
ekg.out
一个整数,不同的合法握手方案。
样例输入1
2
样例输出1
1
样例解释1
只有2个人,所以他们握手肯定是合法的。
样例输入2
4
样例输出2
2
样例解释
如下有3个图,前2个图的握手是合法的,第3种方案是非法的。
分析
很显然,从这两个样例我们并不能看出什么
那么,我们就好好的分析一下:
如图,我们画一个人数为六的场面
我们可以先连接两个点,如下:
图1
图2
图3
图4
图5
可以发现:图3和图4中描述的握手方式是不合法的,因为如图3,2无论和谁握手都会与1,3的直线相交,图4中3无论与谁连都会与1,5相交。
那么,我们来分析一下其它三张图:
图1中1和2连接后,剩下了四个点,在同一边。
图2中1和3连接后,也剩下了四个点,也在同一边。
图5中1和6连接后,剩下四个点,但有两边,两边都有两个点。
设6个点时方案数为
我们可以得出图1中连接两个点剩下人握手的方式即为
,图2同上,图5中两边方案数分别为
根据乘法原理,这四个点总共方案数为
从前面可得
,
,根据加法原理,即一共为
,结果等于5;
以此类推,当
为8时,方案数为14。
1,1,2,5,14……
是不是有点熟悉?
嗯,这就是数学界大名鼎鼎的卡特兰数(又称卡塔兰数)
而这个序列中每一项推出来的方式是:
而在这题里,我们每一项相差2,所以上面式子中的1要改成2,
要改成
.
剩下的详见代码
Code
#include<bits/stdc++.h>
using namespace std;
int n;
long long f[55]; //十年OI一场空,不开long long见祖宗
int main(){
freopen("shake.in","r",stdin);
freopen("shake.out","w",stdout);
scanf("%d",&n);
f[0]=1;
for(int i=2;i<=n;i+=2) {
for(int l=0;l<=i-2;l+=2){
f[i]+=f[l]*f[i-2-l]; //卡特兰数,这是此题代码实现方式
}
}
printf("%lld",f[n]); //用scanf输出long long类型记得%lld
return 0;
}
4、 海狸先生,排队!(ekg)
题意描述
聪明的海狸先生病了,所以他得去看医生。医生叫海狸先生去照心电图,但是心电图室门前排起了好长的队。海狸先生只好跟着排了队。
站了3小时后,聪明的海狸先生发现很多人并不清楚谁应该站在他们自己前面,这可导致队伍变得一团糟。于是海狸先生走到每个人面前,问他在队伍中他前面应该是谁。当然,有人是不知道的,那他可能下一个就能进心电图室,也可能还要等很长很长的时间……
正如你猜到的那样,海狸先生想叫你根据他问问题的结果确定他自己在队伍中的所有可能的位置。
(所以他这么聪明为什么要问我呢)
输入格式
shake.in
第一行有两个正整数n和x(1<= x<= n),代表队伍的总人数和海狸先生的编号。海狸先生用1~n编号所有队伍中的人。
下一行有n个正整数a1, a2,…, an(0 <=ai<=n),ai表示第i个人前面的人的编号,ai = 0表示不知道。数据保证没有环,且任何一个人最多只会被一个人跟在后面。
输出格式
shake.out
按照从小到大的顺序输出海狸先生所有可能的位置。
输入样例1
6 1
2 0 4 0 6 0
输出样例1
2
4
6
输入样例2
6 2
2 3 0 5 6 0
输出样例2
2
5
数据范围
对于30%的数据,最多有20个人不知道自己前面是谁。
对于100%的数据,n <= 1000
分析
代表的是第 个人前面的人编号,那么我们可以反过来,用另一个数组 来表示第 个人后面的人是谁。
我们可以这样想:如果这个人前面的人编号为0,即这个人不清楚自己前面是谁,我们只知道他后面的人,那么他,他后面的人,他后面的后面的人…可以形成一条链,我们将这些链的长度加到一个数组 中。
而我们还知道海狸先生的编号,那我们需要一个特判,判定海狸先生在哪条链,在链的第几个,然后不将海狸先生所在的链长度加入 数组中(为什么后面会提);
那么此时我们知道了,除海狸先生所在链之外所有链的长度。此时,我们开一个 类型的数组 ,这个数组用来干什么呢?是这样的:
我们已经知道所有除海狸先生所在链之外所有链的长度,并将它们存在数组
中,那么我们用一个
来表示这些链的长度是否可以组成长度为
的队列,那么我们就将它转成了一个01背包。
此时如果原本
就可以被构成,那么
就是
,如果
减去一条链长度后得到的值可以被构成,那么此时
也是
。这两个条件满足一个,就可以判定
为
。
可以得出如下代码
f[0]=true; //一定要初始化f[0]
for(int i=1;i<=cur;i++){
for(int j=n;j>=c[i];j--){
if(f[j]==true || f[j-c[i]]==true) //满足一个即可。
f[j]=true; //f[j]即为true。
}
}
最后如果 为 输出 加上海狸先生所在位置,就是答案了。
Code
#include<bits/stdc++.h>
using namespace std;
int m[1005]; //记录每个人后面
int n,x; //如题
int a[1005]; //如题
int c[1005]; //储存除海狸先生所在的链之外所有链的长度
int sum; //每次搜索储存链的长度
int xc=-1; //记录海狸先生的位置
bool xcy; //用于找到海狸先生后特判
int cur=0; //记录链的个数
bool f[1005]; // //又长又臭的变量
void dfs(int k){
sum++;
//printf("%d ",sum);
if(k==x){
xc=sum; //如果搜到海狸先生,记录他在链中的位置。
xcy=true;
return;
}
if(m[k]) dfs(m[k]); //如果后面还有人,继续搜
return;
} //搜索程序
int main(){
freopen("ekg.in","r",stdin);
freopen("ekg.out","w",stdout);
scanf("%d%d",&n,&x);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
m[a[i]]=i;
} //输入每个人前面是谁,这个人前面的后面就是他。
for(int i=1;i<=n;i++){
if(!a[i]){
sum=0;
xcy=false;
dfs(i);
if(!xcy){
c[++cur]=sum; //储存搜到链的长度
}//printf(d"\n"); //如果他前面没有人,就搜索,记得特判。
}
}
//for(int i=1;i<=cur;i++) printf("%d ",c[i]);
f[0]=true;
for(int i=1;i<=cur;i++){
for(int j=n;j>=c[i];j--){
if(f[j]==true || f[j-c[i]]==true)
f[j]=true; //刚才提到的DP程序
}
}
for(int i=0;i<=n;i++){
if(f[i]) printf("%d\n",i+xc); //输出
}
return 0;
}
至此,完美结束