Week 1 训练篇

Week 1 总结

训练篇 :

Round 1:

第一轮的题目都比较简单

1.工作安排

http://47.95.147.191/contest/5/problem/A

描述

Farmer John 有太多的工作要做!为了让农场高效运转,他必须靠他的工作赚钱,每项工作花一个单位时间。

他的工作日从0时刻开始,有个单位时间。在任一时刻,他都可以选择编号 1 ~NN 项工作中的任意一项工作来完成。 因为他在每个单位时间里只能做一个工作,而每项工作又有一个截止日期,所以他很难有时间完成所有 N 个工作,虽然还是有可能。 对于第 i 个工作,有一个截止时间,如果他可以完成这个工作,那么他可以获利

在给定的工作利润和截止时间下,FJ能够获得的利润最大为多少呢?

输入

  • 第1行:一个整数N. 第2~N+1行:第i+1行有两个用空格分开的整数:.

输出

  • 输出一行,里面有一个整数,表示最大获利值。

这个题目就是建立一个利润的最大生成树,然后从最后一天开始往前操作。因为每一天可以完成DDL是之后的工作而不能DDL是完成之前的。

#include<stdio.h>
#include<algorithm>
#include<queue>
#include<iostream>
using namespace std;
struct VD{
long long d,p;
bool operator < (const VD & t)const{
return t.p > p;
}
}tm[110000];
bool cmp(const VD & i , const VD & j ){
return i.d>j.d;
}
long long ans=0,n;
int main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld %lld",&tm[i].d,&tm[i].p);
}
priority_queue<VD>q;
sort(tm+1,tm+n+1,cmp);
for(int i=1;i<=n;){
int ddl=tm[i].d;
while(tm[i].d==ddl){
q.push(tm[i]);
i++;
}
int td;
td=tm[i-1].d-tm[i].d;
while(td--){
if(!q.size()) break;
VD temp=q.top();
q.pop();
ans+=temp.p;
}
}
printf("%lld",ans);
return 0;
}

 

2.轻拍牛头

http://47.95.147.191/contest/5/problem/B

描述

今天是贝茜的生日,为了庆祝自己的生日,贝茜邀你来玩一个游戏.

贝茜让N 头奶牛坐成一个圈.除了1号与N号奶牛外,i号奶牛与i−1号和i+1号奶牛相邻.N号奶牛与1号奶牛相邻.农夫约翰用很多纸条装满了一个桶,每一张包含了一个独一无二的1到的数字.

接着每一头奶牛i从桶中取出一张纸条.每头奶牛轮流走上一圈,同时拍打所有编号能整除在纸条上的数字的牛的头,然后做回到原来的位置.牛们希望你帮助他们确定,每一头奶牛需要拍打的牛.

输入

第1行包含一个整数N,接下来第2到N+1行每行包含一个整数

输出

第1到N行,每行的输出表示第i头奶牛要拍打的牛数量.

题目意思其实就是找到所有奶牛中编号可以被整除的数量,于是就想到了筛法。

#include<stdio.h>
#include<math.h>
#include<algorithm>
#include<iostream>
using namespace std;
int n,a[110000],at[110000],st[1100000],s[1100000];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
at[i]=a[i];
st[a[i]]++;
}
sort(a+1,a+n+1);
int cnt=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=cnt;i++){
for(int j=1;j<=sqrt(a[i]);j++){
if(a[i]%j==0){
if(j*j==a[i]) {
s[a[i]]+=st[j];
}
else {
s[a[i]]+=st[j];
s[a[i]]+=st[a[i]/j];
}
}
}
}
for(int i=1;i<=n;i++){
printf("%d ",s[at[i]]-1);
}
return 0;
}

上面是第一遍的做法,因为不熟悉筛法所以做得有点蠢。。当时就看到怎么别人时间都是40多,我是260多。然后又想了一会才想到找素数中的用到的埃筛。

#include<stdio.h>
#include<algorithm>
#include<iostream>
using namespace std;
int n,a[1100000],x[1100000],s[1100000],maxt=0,num[1100000];
int main(){

scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
maxt=max(maxt,a[i]);
x[i]=a[i];num[a[i]]++;
}
sort(a+1,a+n+1);
int cnt=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=cnt;i++)
for(int j=a[i];j<=maxt;j+=a[i]){
s[j]+=num[a[i]];
}
for(int i=1;i<=n;i++){
printf("%d ",s[x[i]]-1);
}
return 0;
}

更简洁还更快。。。

 

3.奶牛的比赛

http://47.95.147.191/contest/5/problem/C

描述

FJ的N 头奶牛们最近参加了场程序设计竞赛。在赛场上,奶牛们按1..N依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。

整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为A的奶牛的编程能力强于编号为B的奶牛 ,那么她们的对决中,编号为A的奶牛总是能胜出。

FJ想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 M 轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。

输入

  • 第1行: 2个用空格隔开的整数:NM

  • 第2..M+1行: 每行为2个用空格隔开的整数A、B,描述了参加某一轮比赛的奶牛的编号,以及结果(编号为A,即为每行的第一个数的奶牛为 胜者)

输出

  • 第1行: 输出1个整数,表示排名可以确定的奶牛的数目

这个题目可以化成有向图的问题给出M条边,然后建图对于第i个点,记录有 l 个点可以到 i 就说明有 l 头奶牛排名比i高,然后记录 i 可以到达 r 个点,说明 i 排名比 r 头奶牛高。最后如果就说明这头牛的排名可以确定。问题就成了找路径,由于N很小,就可以想到用floyd找路径,然后开两个数组记录可以到每个点的点的个数和每个点可以到达的点的个数。

#include<stdio.h>
#include<iostream>
using namespace std;
int ans=0,t,n,m[110][110],l[110],r[110];
int main(){
for(int i=1;i<=105;i++)
for(int j=1;j<=105;j++){
m[i][j]=100000;
}
scanf("%d%d",&n,&t);
for(int i=1;i<=t;i++){
int a,b;
scanf("%d%d",&a,&b);
m[a][b]=1;
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
m[i][j]=min(m[i][j],m[i][k]+m[k][j]);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(m[i][j]<10000){
l[j]++;r[i]++;
}
}
for(int i=1;i<=n;i++){
//printf("%d---(%d,%d) ",i,l[i],r[i]);
if(l[i]+r[i]==n-1) ans++;
}
printf("%d",ans);
return 0;
}

 

4.饥饿的奶牛

http://47.95.147.191/contest/5/problem/D

描述

Farmer John养了N头奶牛,每头牛都有一个不超过32位二进制数的正整数编号。

FJ希望奶牛们在进食前,能按编号从小到大的顺序排好队,但奶牛们从不听他的话。为了让奶牛们养成这个习惯,每次开饭时,FJ从奶牛中顺序地挑出一些,这些奶牛的编号必须按挑出的顺序递增。然后FJ让被挑出的奶牛们吃饭——其他奶牛就只能饿肚子了。

现在,你得到了这一次开饭前队伍中从前到后所有奶牛的编号。奶牛们想请你计算一下,按照FJ的规定,最多有多少头奶牛能吃上饭?

比如说,有11头奶牛按以下顺序排好了队(数字代表奶牛的编号) 对于这个队列,最多可以让7头奶牛吃上饭,她们的编号分别为。队列是不合法的,因为第3头奶牛的编号(3)小于她前面一头奶牛的编号(5)。

输入

  • 第1行: 一个整数,N

  • 第2行: N 个数,依次表示队伍中从前到后的奶牛的编号。

输出

  • 第1行: 输出按照FJ的规定,最多可以挑出的奶牛的数目

题目很明显就是求最长上升子序列。

#include<stdio.h>
#include<iostream>
using namespace std;
long long n,a[5100];
int f[5100],maxt=0;
int main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
f[1]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=i;j++){
if(a[i]>a[j]){
f[i]=max(f[i],f[j]+1);
}
}
}
for(int i=1;i<=n;i++){
maxt=max(maxt,f[i]);
}
printf("%d",maxt);
return 0;
}

需要注意的是for(int j=0;j<=i;j++)这一层循环 j 是从0开始的,至于为什么,自己写写画画一组数据应该就可以明白。

 

 

Round 2:

第二轮是DP专题,难度比第一轮大。而且我DP没学好,做后面两道题花了好久。

1.开心的金明

http://47.95.147.191/contest/7/problem/A

描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为5等:用整数1−5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为,则所求的总和为:。请你帮助金明设计一个满足要求的购物单。

输入

第一行,为22个正整数,用一个空格隔开:n m(其中N(<30000)表示总钱数,m(<25)为希望购买物品的个数。)

从第2行到第m+1行,第j行给出了编号为j−1的物品的基本数据,每行有2个非负整数v p(其中v表示该物品的价格p表示该物品的重要度(1-5)。

输出

1个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<100000000)。

这个题没什么好说的,就是0-1背包。

#include<stdio.h>
#include<iostream>
using namespace std;
int m,n,w[30],v[30],f[300001];
int main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
int t;
scanf("%d%d",&w[i],&t);
v[i]=w[i]*t;
}
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--){
if(w[i]<j) f[j]=max(f[j],f[j-w[i]]+v[i]);
}
printf("%d",f[m]);
return 0;
}

 

2.金明的预算方案

http://47.95.147.191/contest/7/problem/B

描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

QQ截图20200225232607.png

主件附件电脑打印机,扫描仪书柜图书书桌台灯,文具工作椅无如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有0个、1个或2个附件。附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为5等:用整数1−5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是10元的整数倍)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为,则所求的总和为:。请你帮助金明设计一个满足要求的购物单。

输入

第1行,为两个正整数,用一个空格隔开:N m(其中N(<32000)表示总钱数,m(<60)为希望购买物品的个数。)

从第22行到第m+1行,第j行给出了编号为j−1的物品的基本数据,每行有3个非负整数v p q(其中v表示该物品的价格(v<10000),p表示该物品的重要度(1−5),q表示该物品是主件还是附件。如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号)

输出

一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)

这是应该0-1背包的变种问题,就是把取或不去两种决策变成了取不取主件,然后取不取附件,还有去一个附件还是取两个附件。其实也可以看做是树状DP。我是分五种决策做的。下面有树状DP的问题。

#include<stdio.h>
#include<iostream>
using namespace std;
int m,n,w[100],v[100],f[40000],q[100][3],ppt[100];//q[i][j]表示第i个物体的第(j-1)个附件编号
int main(){  //ppt[i]表示第i个物体是不是主件
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
int t;
scanf("%d%d%d",&w[i],&t,&ppt[i]);
v[i]=w[i]*t;
if(q[ppt[i]][0]) q[ppt[i]][1]=i;
else q[ppt[i]][0]=i;
}
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--){
if(!ppt[i]){
if(w[i]<=j) f[j]=max(f[j],f[j-w[i]]+v[i]);
               
if(w[i]+w[q[i][0]]<=j) f[j]=max(f[j],f[j-w[i]-w[q[i][0]]]+v[i]+v[q[i][0]]);
               
if(q[i][1]){
if(w[i]+w[q[i][1]]<=j) f[j]=max(f[j],f[j-w[i]-w[q[i][1]]]+v[i]+v[q[i][1]]);
               
if(w[i]+w[q[i][0]]+w[q[i][1]]<=j)
           f[j]=max(f[j],f[j-w[i]-w[q[i][0]]-w[q[i][1]]]+v[i]+v[q[i][0]]+v[q[i][1]]);
}
}
}
printf("%d",f[m]);
return 0;
}

 

3.逆序对数列

http://47.95.147.191/contest/7/problem/C

描述

对于一个数列,如果有 i<j ,那么我们称 为一对逆序对数。若对于任意一个由 1~n 自然数组成的数列,可以很容易求出有多少个逆序对数。

那么逆序对数为 k 的这样自然数数列到底有多少个?

输入

第一行为两个整数 n,k

输出

写入一个整数,表示符合条件的数列个数,由于这个数可能很大,你只需输出该数对10000求余数后的结果。

这个题目找递推公式就好了。

表示从1到 i 逆序对为 j 的个数。我们首先要意识到每多加一个数,新加入的数是最大的因此这个数往前移动多少位就会增加多少个逆序对。

注意,新加入的那个数的移动是有限制的,最多只能移动位。(这一点我开始没有意识到,然后wa了。)

 
#include<stdio.h>
#include<iostream>
using namespace std;
const int mod = 10000;
int n,k,f[1100][1100],sum[1100];//sum是前缀和,是一个滚动数组。对每一个数都会重新开始储存。
int main(){
scanf("%d%d",&n,&k);
sum[0]=1;
f[2][1]=1;
for(int i=3;i<=n;i++)
for(int j=1;j<=k;j++){
sum[j]=(sum[j-1]+f[i-1][j])%mod;
f[i][j]=sum[j];
if(j>i-1){
f[i][j]=(f[i][j]-sum[j-i]+mod)%mod;
}
}
/*for(int i=1;i<=n;i++)
{
for(int j=1;j<=k;j++){
printf("(%d,%d)--%d ",i,j,f[i][j]);
}
}*/
printf("%d",f[n][k]);
return 0;
}

 

4.选课

http://47.95.147.191/contest/7/problem/D

描述

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有N门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程a是课程b的先修课即只有学完了课程a,才能学习课程b)。一个学生要从这些课程里选择M门课程学习,问他能获得的最大学分是多少?

输入

第一行有两个整数N,M用空格隔开。接下来的N行,第i+1行包含两个整数,表示第I门课的直接先修课,表示第 i 门课的学分。若=0表示没有直接先修课()

输出

只有一行,选M门课程的最大得分。

这个题一看就知道是树状DP。找到父子之间的转移方程就行了,由于规定要选M门课,所以是在 i 节点处选了j门课的最大学分(从子节点中选的)。

要注意的是,这里的depth是 i 这个点的子节点个数。

 
#include<stdio.h>
#include<iostream>
using namespace std;
int m,n,dep=0,num=0,head[1000],f[110][110];
struct EDGE{
int w,to,nxt;
}a[1000];
void add(int f , int t , int dis){
a[++num].nxt=head[f];
a[num].to=t;
a[num].w=dis;
head[f]=num;
}
void dfs(int x){
for(int i=head[x];i;i=a[i].nxt){
int v=a[i].to;
dfs(v);
for(int j=min(m,dep);j>=0;j--){
for(int k=j-1;k>=0;k--){
f[x][j]=max(f[x][j],f[x][j-k-1]+f[v][k]+a[i].w);
}
}
}
dep++;
}

int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
int u,d;
scanf("%d%d",&u,&d);
add(u,i,d);
}
dfs(0);
printf("%d",f[0][m]);
return 0;
}

 

5.牧场的安排

http://47.95.147.191/contest/7/problem/E

描述

Farmer John 新买了一块长方形的牧场,这块牧场被划分成NM,每一格都是一块正方形的土地。FJ 打算在牧场上的某几格土地里种上美味的草,供他的奶牛们享用。遗憾的是,有些土地相当的贫瘠,不能用来放牧。并且,奶牛们喜欢独占一块草地,于是 FJ 不会选择两块相邻的土地,即:没有哪两块草地有公共边。当然,FJ 还没有决定在哪些土地上种草。

作为一个好奇的农场主,FJ 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择。当然,把新的牧场荒废,不在任何土地上种草,也算一种方案。请你帮 FJ 算一下这个总方案数。

输入

第1行:两个正整数M和N,用空格隔开;

第2到M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。输入的第i + 1行描述了第i行的土地。所有整数均为0或1, 1表示这块土地足够肥沃,0则表示这块地上不适合种草。

输出

第1行:输出一个整数,即牧场分配总方案数除以的余数。

这个题也是很清楚,状压DP。在一个二维动态规划问题中总状态比较复杂但是每个元素的状态又不复杂的时候,我们可以用一个二进制数表示一行的状态。像这道题很明显,就当草地为0是对应二进制位置为0,1也是一样。我们就可以记录每一行的状态。然后再用位运算的只是去转移表示出转移方程。

#include<stdio.h>
#include<iostream>
using namespace std;
const int mod = 100000000;
int ans=0,m,n,state[1500],tot=0,f[15][1500],cnr[15];

bool fit(int x , int k){ //fit函数用来判断我们枚举的状态是否合法,就是这一行是否可以表示
return !(state[x]&cnr[k]); //成这种状态。
}

void intnt(){ //state数组存的是一行中所有满足不存在相邻的情况(假设全为1)
for(int i=0;i<1<<n;i++){
if(!(i&(i<<1))) state[++tot]=i;
}
}

int main(){
cin>>m>>n;
intnt();
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
int t;
cin>>t;
if(!t) cnr[i]+=1<<(n-j);
}
for(int i=1;i<=tot;i++){ //初始化第一行。
if(fit(i,1)){
f[1][i]=1;
}
}
for(int i=2;i<=m;i++){ //从第二行开始枚举每一行的状态。
for(int j=1;j<=tot;j++){
if(fit(j,i)){
for(int k=1;k<=tot;k++){
if(fit(k,i-1)){
if(!(state[j]&state[k])){
f[i][j]+=f[i-1][k];
f[i][j]=f[i][j]%mod;
}
}
}
}
}
}
for(int i=1;i<=tot;i++){
ans=(ans+f[m][i])%mod;
}
cout<<ans<<endl;
return 0;
}

从这个代码可以找到一些常用的操作:

for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
int t;
cin>>t;
if(!t) cnr[i]+=1<<(n-j);
}

这里存cnr数组时是当该位置草地为0时二进制对应位置为1。为什么这样存呢?

bool fit(int x , int k){            //fit函数用来判断我们枚举的状态是否合法,就是这一行是否可以表示
return !(state[x]&cnr[k]); //成这种状态。
}

来看这个fit函数,当statecnr不存在同一个位置同为1,也就是说,state即我们所枚举的状态中为1的位置对应cnr这一行的状态为0的位置。而我们存cnr的时候是反着存的,所以state所有的1位置都在这一行为1的位置。我觉得这个操作很厉害(因为我太菜了吧)。

            if(fit(k,i-1)){
if(!(state[j]&state[k])){
f[i][j]+=f[i-1][k];
f[i][j]=f[i][j]%mod;
}
}

然后这个转移的位置就比较基本了,理解了fit函数之后这里就很好理解了。是我们枚举的第i行的状态,是枚举的上一行的位置,意味着这个状态是可以出现在上一行的。然后就表示这两个状态没有位置同为1,即这两行没有相邻的位置都种了草坪。然后就可以转移了。

哎,还是太菜了,这种类型的题做少了。

总结:

动态规划还是不熟练,特别是树状DP,状压DP。然后做了这些题的收获也是有的。动态规划除了要找到转移方程还要找到范围,边界状态。还有就是树状DP,状压DP这方面的知识增加了。状压DP需要很灵活的用到一系列位运算的知识。总之就是要多做题就是了。

原文地址:https://www.cnblogs.com/xcsj/p/12397835.html