1003 动规专练 解题报告
T1 卡车更新问题 (文件名:truck.pas/c/cpp)
【题目描述】
某人购置了一辆新卡车, 从事个体运输业务. 给定以下各有关数据: R[t], t=0,1,2,...,k, 表示已使用过 t 年的卡车, 再工作一年所得的运费, 它随 t 的增加而减少, k (k≤20) 年后卡车已无使用价值.
U[t]: t=0,1,...,k, 表示已使用过 t 年的卡车, 再工作一年所需的维修费, 它随 t 的增加而增加.
C[t], t=0,1,2,...,k, 表示已使用过 t 年的旧卡车, 卖掉旧车, 买进新车, 所需的净费用, 它随 t 的增加而增加. 以上各数据均为实型, 单位为"万元".
设某卡车已使用过 t 年,
① 如果继续使用, 则第 t+1 年回收额为 R[t]-U[t],
② 如果卖掉旧车,买进新车, 则第 t+1年回收额为 R[0]-U[0]-C[t] .
该运输户从某年初购车日起,计划工作 N (N<=20) 年, N 年后不论车的状态如何,不再工作. 为使这 N 年的总回收额最大, 应在哪些年更新旧车? 假定在这 N年内, 运输户每年只用一辆车, 而且以上各种费用均不改变.
【输入数据】
第 1 行: N (运输户工作年限)
第 2 行: k (卡车最大使用年限, k≤20 )
第 3 行: R[0] R[1] ... R[k]
第 4 行: U[0] U[1] ... U[k]
第 5 行: C[0] C[1] ... C[k]
【输出数据】
第 1 行: W ( N 年总回收额 )
第 2--N+1 行: 每行输出 3 个数据:
年序号 ( 从 1 到 N 按升序输出 );
否更新 ( 当年如果更新,输出 1, 否则输出 0);
当年回收额 ( N 年回收总额应等于 W ).
【吐槽】
临场去掉了输出方案,整个题就水了。(借用Cydiater的话:输出方案就是毒瘤)
用f[i][j]表示前i年,当前车使用了j年的总回收额,则
当j==1时,f[i][j]=max{f[i][j],f[i-1][k]+R[0]-U[0]-C[k]} (1<=k<=min(i-1,m))
否则,f[i][j]=f[i-1][j-1]+R[j-1]-U[j-1] (2<=j<=min(i,m))
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<ctime> 7 #include<algorithm> 8 using namespace std; 9 #define MAXN 30 10 struct node{int flag;double v;}ANS[MAXN]; 11 int n,m; 12 double ans,R[MAXN],U[MAXN],C[MAXN],f[MAXN][MAXN]; 13 inline int read() 14 { 15 int x=0,f=1; char ch=getchar(); 16 while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} 17 while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} 18 return x*f; 19 } 20 double Max(double a,double b) {if(a>b) return a; else return b;} 21 int main() 22 { 23 freopen("truck.in","r",stdin); 24 freopen("truck.out","w",stdout); 25 n=read(); m=read(); 26 for(int i=0;i<=m;i++) scanf("%lf",&R[i]); 27 for(int i=0;i<=m;i++) scanf("%lf",&U[i]); 28 for(int i=0;i<=m;i++) scanf("%lf",&C[i]); 29 f[1][1]=R[0]-U[0]; 30 for(int i=2;i<=n;i++) 31 for(int j=1;j<=min(i,m);j++) 32 { 33 if(j==1) 34 { 35 for(int k=1;k<=min(i-1,m);k++) 36 f[i][j]=Max(f[i][j],f[i-1][k]+R[0]-U[0]-C[k]); 37 } 38 else f[i][j]=f[i-1][j-1]+R[j-1]-U[j-1]; 39 } 40 for(int i=1;i<=m;i++) ans=Max(ans,f[n][i]); 41 printf("%.1lf\n",ans); 42 return 0; 43 }
T2 打扫卫生 (文件名:cleanup.pas/c/cpp)
【问题描述】
很久很久以前,Framer John只会做一种食品;而现在John能给他的N(1<=N<=40000)只奶牛供应M(1<=M<=N)种不同的食品。奶牛们非常挑剔,i号奶牛只吃食品Pi(1<=Pi<=M)。
每天,奶牛们按编号排队进自助餐厅用餐。不幸的是,这么多各类的食品让清扫工作变得非常复杂。当John供应K种食品,他之后就需要K^2的时间进行清扫。
为了减少清扫的时间,John决定把排好队的奶牛划分成若干组。每一组包含一段连续的奶牛,每一次,只让一组奶牛进入餐厅。这样,他可以让清扫所需的总用时变得最小。请计算这个最小用时。
【输入】
第一行,两个整数,N和M。
接下来N行,第i个整数位Pi。
【输出】
一个整数S,表示最小话费的时间。
【暴力法】
预处理出sum数组,sum[i][j]表示从第i到j头奶牛不同的食品数,f[i]表示前i头奶牛花费的最小时间,
状态转移方程:f[i]=min{f[i],f[i-k]+sum[i-k+1][i]^2} (1<=k<=i)
时间复杂度:O(n^2) 可以得到50分。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<ctime> 7 #include<algorithm> 8 using namespace std; 9 #define MAXN 5010 10 long long n,m,a[MAXN],f[MAXN],check[MAXN],sum[MAXN][MAXN]; 11 inline long long read() 12 { 13 long long x=0,f=1; char ch=getchar(); 14 while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} 15 while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} 16 return x*f; 17 } 18 int main() 19 { 20 freopen("cleanup.in","r",stdin); 21 freopen("cleanup.out","w",stdout); 22 memset(f,10,sizeof(f)); 23 n=read(); m=read(); 24 for(long long i=1;i<=n;i++) a[i]=read(); 25 for(long long i=1;i<=n;i++) 26 { 27 memset(check,0,sizeof(check)); 28 sum[i][i]=1; check[a[i]]=1; 29 for(long long j=i+1;j<=n;j++) 30 { 31 sum[i][j]=sum[i][j-1]+(check[a[j]]?0:1); 32 check[a[j]]=1; 33 } 34 } 35 f[1]=1; f[0]=0; 36 for(long long i=2;i<=n;i++) 37 for(long long k=1;k<=i;k++) 38 f[i]=min(f[i],f[i-k]+sum[i-k+1][i]*sum[i-k+1][i]); 39 printf("%I64d\n",f[n]); 40 return 0; 41 }
【奇妙的正解】
可以发现每一段中不同的数的个数不超过sqrt(n)个。
所以我们只需要记录每个点为结尾,一段序列中不同的数的个数不超过j的左端点的位置-1,pos[j]。通过pos数组进行DP转移,可以将复杂度从O(n^2)将为O(sqrt(n)n)。
状态转移方程:f[i]=min{f[pos[j]]+j*j}。
下面我们考虑右端点右移,也就是i++时,如何更新pos数组。
为了快速更新,我们还需要记录每个数值i的最后一个位置pre[i],和以pos[j]为左端点的序列中不同的数的个数cnt[j]。
当i++后,如果pre[a[i]]≤pos[j],那么cnt[j]++,说明序列中加入一个新的元素。
然后我们找出所有cnt[j]>j的序列,也就是不满足条件的序列,适当地调整左端点pos[j],使其满足cnt[j]≤j。其中对于左端点的调整只需要暴力即可,这是均摊O(1)的。
所以这道题最终的时间复杂度为O(n*sqrt(n))。
——正解转自 http://blog.csdn.net/aarongzk/article/details/50756718
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<ctime> 7 #include<algorithm> 8 using namespace std; 9 #define MAXN 40010 10 int n,m,a[MAXN],pos[MAXN],cnt[MAXN],pre[MAXN],f[MAXN]; 11 inline int read() 12 { 13 int x=0,f=1; char ch=getchar(); 14 while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} 15 while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} 16 return x*f; 17 } 18 int main() 19 { 20 freopen("cleanup.in","r",stdin); 21 freopen("cleanup.out","w",stdout); 22 n=read(); m=read(); m=(int)sqrt(n*1.0); memset(pre,-1,sizeof(pre)); 23 for(int i=1;i<=n;i++) a[i]=read(); 24 for(int i=0;i<=n;i++) f[i]=i;//f的已知最小值 25 for(int i=1;i<=n;i++) 26 { 27 for(int j=1;j<=m;j++) if(pre[a[i]]<=pos[j]) cnt[j]++; 28 pre[a[i]]=i; 29 for(int j=1;j<=m;j++) 30 if(cnt[j]>j) 31 { 32 int t=pos[j]+1; 33 while(pre[a[t]]>t) t++; 34 pos[j]=t; 35 cnt[j]--; 36 } 37 for(int j=1;j<=m;j++) f[i]=min(f[i],f[pos[j]]+j*j); 38 } 39 printf("%d\n",f[n]); 40 return 0; 41 }
T3 工作计划 (文件名:wrk.pas/c/cpp)
【题目描述】
Mark 在无意中了解到了 Elf 的身世。在和 James 商量过之后,好心的他们打算送 Elf 返回故乡。然 而,去往 Gliese 的飞船票价高的惊人,他们暂时还 付不起这笔费用。经过一番考虑,Mark 打算去额外 做一些工作来获得收入。
经过一番调查,Mark 发现有 N 个工作可以做。 做第 i 件工作所需要的时间为 Di,同时也需要一个能力值 Ci 才可以去做,每件工作都可以在任意时间开始,也可以做任意多次。所有的工作 给付的报酬都是一致的。同时,有 S 个课程可以参加,我们认为今天是第 0 天,第 i 个课程 在第 Mi 天开始,持续时间为 Li 天,课程结束之后能力值会变为 Ai。现在 Mark 的能力值为1。Mark 只能做工作到第 T 天(因为那是飞船起飞的日子)。 他想知道期限内他最多可以做多少件工作,好决定未来的打算。于是他找到了 applepi。
然而 applepi 还差一个题没有出,所以这个任务就交给你了。
【输入格式】
第一行包含三个空格分隔的整数 T,S,N。
之后 S 行,每行三个整数 M,L,A,描述一个课程。 之后 N 行,每行两个整数 C,D,描述一件工作。
【输出格式】
一个整数,表示 Mark 最多可以做多少件工作。
【吐槽】
写题时把k写成了i,光荣爆掉。。。。。。
题解:动态规划,定义f[i][j]代表在i时间,能力值为j的最多工作次数。
对应最后三种选择:
①不作为 f[i][j]=f[i-1][j],
②上课 f[i][j]=f[上课前一个时刻][任意],
③做工作 f[i][j]=f[i-po[j]][j]+1 (po[j]为能力值<=j的工作一次的最短用时)。
对于②可以在预处理出ke[i][j]在i时刻结束,能力值达到j的课程的最晚开始时间。dp过程中处理出g[i]=max{f[i][j]}。
g[t]即为答案。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<ctime> 7 #include<algorithm> 8 #include<queue> 9 using namespace std; 10 int t,s,n,po[105],g[10005],f[10005][105],ke[10005][105]; 11 inline int read() 12 { 13 int x=0,f=1; char ch=getchar(); 14 while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} 15 while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} 16 return x*f; 17 } 18 int main() 19 { 20 freopen("wrk.in","r",stdin); 21 freopen("wrk.out","w",stdout); 22 memset(f,128,sizeof f); 23 t=read(); s=read(); n=read(); 24 for(int i=1;i<=s;i++) 25 { 26 int m=read(),l=read(),a=read(); 27 ke[m+l-1][a]=max(ke[m+l-1][a],m); 28 } 29 memset(po,0x3f,sizeof po); 30 for(int i=1;i<=n;i++) 31 { 32 int c=read(),d=read(); 33 for(int j=c;j<=100;j++) po[j]=min(po[j],d); 34 } 35 f[0][1]=0; g[0]=0; 36 for(int i=1;i<=t;i++) 37 for(int j=1;j<=100;j++) 38 { 39 f[i][j]=f[i-1][j]; 40 if(ke[i-1][j])f[i][j]=max(f[i][j],g[ke[i-1][j]]); 41 if(i-po[j]>=0)f[i][j]=max(f[i][j],f[i-po[j]][j]+1); 42 g[i]=max(g[i],f[i][j]); 43 } 44 printf("%d\n",g[t]); 45 return 0; 46 }
T4 探险队 (exp.pas/c/cpp)
【题目描述】
N个探险家组队去探索神秘的金字塔。 在路上有个 Stuff 问每个探险家参加过的探险活动的数目。 而探险家们都不愿意告诉他。 于是这个Stuff得到的回答都是“在这 N 个探险家中,一共 A个探险家参加过的探险活动比我多,而 B 个探险家参加过的探险活动比我少。 ” 当然并不一定所有的探险家都说了实话。现在这个悲催的 Stuff 想请你帮助他计算至少有多少个探险家说了谎。
【输入格式】
第一行是一个正整数 N,表示探险队的人数。
之后 N行每行两个整数 A和 B,表示一个探险家的答案。
【输出格式】
输出一个整数表示答案,即至少有多少人说了谎。
【题解】
HAOI2011 problem a
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<ctime> 7 #include<algorithm> 8 using namespace std; 9 #define MAXN 100010 10 struct node{int l,r;}e[MAXN]; 11 int n,len,l[MAXN],r[MAXN],v[MAXN],f[MAXN]; 12 bool cmp(node a,node b) {return a.r==b.r?(a.l<b.l):(a.r<b.r);} 13 inline int read() 14 { 15 int x=0,f=1; char ch=getchar(); 16 while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} 17 while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} 18 return x*f; 19 } 20 int main() 21 { 22 freopen("exp.in","r",stdin); 23 freopen("exp.out","w",stdout); 24 n=read(); 25 for(int i=1;i<=n;i++) 26 { 27 int x=read(),y=read(); 28 e[i].l=x+1; e[i].r=n-y; 29 } 30 sort(e+1,e+n+1,cmp); 31 for(int i=1;i<=n;i++) 32 { 33 if(e[i].l!=e[i-1].l||e[i].r!=e[i-1].r) len++; 34 v[len]++; 35 v[len]=min(v[len],e[i].r-e[i].l+1); 36 l[len]=e[i].l; r[len]=e[i].r; 37 } 38 int j=1; 39 for(int i=1;i<=n;i++) 40 { 41 f[i]=f[i-1]; 42 while(j<=len&&r[j]==i) {f[i]=max(f[i],f[l[j]-1]+v[j]); j++;} 43 } 44 printf("%d\n",n-f[n]); 45 return 0; 46 }
T5 金字塔 (pyr.pas/c/cpp)
【题目描述】
虽然探索金字塔是极其老套的剧情, 但是这一队探险家还是到了某金字塔脚下。经过多年的研究,科学家对这座金字塔的内部结构已经有所了解。首先,金字塔由若干房间组成,房间之间连有通道。如果把房间看做节点,通道看做边的话,整个金字塔呈现一个有根树结构,节点的子树之间有序,金字塔有唯一的一个入口通向树根。并且,每个房间的墙壁都涂有若干种颜色的一种。 探险队员打算进一步了解金字塔的结构,为此,他们使用了一种特殊设计的机器人。这种机器人会从入口进入金字塔, 之后对金字塔进行深度优先遍历。 机器人每进入一个房间 (无论是第一次进入还是返回) ,都会记录这个房间的颜色。最后,机器人会从入口退出金字塔。 显然, 机器人会访问每个房间至少一次, 并且穿越每条通道恰好两次 (两个方向各一次) ,然后,机器人会得到一个颜色序列。但是,探险队员发现这个颜色序列并不能唯一确定金字塔的结构。现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列。由于结果可能会非常大,你只需要输出答案对 109取模之后的值。
【输入格式】
输入文件包含一行,含有一个字符串,表示机器人得到的颜色序列。
【输出格式】
输出一个整数表示答案。
【题解】
区间dp+乘法原理
输入是一个回文串,如果不是回文串,肯定方法数是0.(这也是一个比较好的骗分策略。)
既然要分方法数,那么肯定就和递推或者动态规划有关系。并且数据量肯定满足动态规划,但是搜索肯定会超时。
我们思考一下如果这棵树分为几个叉,那么几个叉的方法数肯定就是一个乘积的关系,一个复杂的问题分为多个步骤来完成。
分成几叉的条件是什么,就是在中间出现和根相同的字符,这个条件在题目中尤为重要。
所以,这道题的代码就是一个基本的 区间dp模板。
这道题的类似题目推荐 oj 1892。Hnoi2010的题目,bzoj上也有的。
也是区间dp求方法数。
1 #include<iostream> 2 #include<string> 3 #include<cstring> 4 #include<cstdio> 5 #include<queue> 6 #include<utility> 7 #include<cmath> 8 #include<algorithm> 9 #include<cstdlib> 10 #include<ctime> 11 #include<set> 12 #include<map> 13 #define ll long long 14 using namespace std; 15 const int maxn=305; 16 const int mod=1000000000; 17 int L; 18 char ch[maxn]; 19 ll f[maxn][maxn]; 20 int main() 21 { 22 freopen("pyr.in","r",stdin); 23 freopen("pyr.out","w",stdout); 24 scanf("%s",ch+1); 25 L=strlen(ch+1); 26 for(int i=1;i<=L;i++) f[i][i]=1; 27 for(int len=2;len<=L;len++) 28 { 29 for(int i=1;i<=L;i++) 30 { 31 int j=i+len-1; 32 if(j>L) break; 33 if(ch[i]==ch[j]&&((j-i+1)&1)) 34 f[i][j]=f[i+1][j-1]; 35 else continue; 36 for(int k=i+2;k<j;k++) 37 f[i][j]=(f[i][j]+f[i+1][k-1]*f[k][j])%mod; 38 } 39 } 40 cout<<f[1][L]<<endl; 41 return 0; 42 }
T6启示录 (apo.pas/c/cpp)
【题目描述】
探险队员终于进入了金字塔。通过对古文字的解读,他们发现,和《圣经》的作者想的一样,古代人认为 666是属于魔鬼的数。不但如此,只要某数字的十进制表示中有三个连续的 6, 古代人也认为这个是魔鬼的数, 比如 666, 1 666, 2 666, 3 666, 6 663, 16 666, 6 660 666 等等, 统统是魔鬼的数。 古代典籍经常用 “第 X大的魔鬼的数” 来指代这些数。这给研究人员带来了极大的不便。 为了帮助他们, 你需要写一个程序来求出这些魔鬼的数字。
【输入格式】
输入文件包含多组测试数据。第一行有一个整数 T 表示测试数据的组数。
每组测试数据包含一个整数 X,表示需要求第 X大的魔鬼的数。
【输出格式】
对于每组测试数据,在一行内输出结果。
数位dp,没学过,留个坑。。。。。。
===========================
今天学了数位dp,把这个坑填上。
首先我们把这个问题转化成另一个问题:给定n,求1~n中有多少个数含有666
解决了这个问题,把原问题二分答案即可
首先预处理f数组,令
f[i][0]表示i位数中首位不为6且不含666的数的数量
f[i][1]表示i位数中首位连续1个6并且不含666的数的数量
f[i][2]表示i位数中首位连续2个6并且不含666的数的数量
f[i][3]表示i位数中含有666的数的数量
于是我们有递推式
f[i][0]=(f[i-1][0]+f[i-1][1]+f[i-1][2])*9;
f[i][1]=f[i-1][0];
f[i][2]=f[i-1][1];
f[i][3]=f[i-1][3]*10+f[i-1][2];
然后数位DP即可
其中处理的时候要记录当前确定数字末尾的6的个数 以及确定数字中是否含有666
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<ctime> 7 #include<algorithm> 8 using namespace std; 9 typedef long long ll; 10 ll T,S,f[30][5],digit[30]; 11 inline ll read() 12 { 13 ll x=0,f=1; char ch=getchar(); 14 while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} 15 while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} 16 return x*f; 17 } 18 void pre() 19 { 20 f[0][0]=1; 21 for(ll i=1;i<=29;i++) 22 { 23 f[i][0]=(f[i-1][0]+f[i-1][1]+f[i-1][2])*9; 24 f[i][1]=f[i-1][0]; 25 f[i][2]=f[i-1][1]; 26 f[i][3]=f[i-1][2]+f[i-1][3]*10; 27 } 28 } 29 int getans(ll x) 30 { 31 ll len=0,ans=0,cnt=0; 32 while(x) {digit[++len]=x%10; x/=10;} 33 for(ll i=len,j;i;i--) 34 { 35 ll sum; 36 for(int j=1;j<=digit[i];j++) 37 { 38 if(cnt==3) sum=3; 39 else if(j==7) sum=cnt+1; 40 else sum=0; 41 for(ll k=3;k>=3-sum;k--) ans+=f[i-1][k]; 42 } 43 if(cnt!=3) cnt=(digit[i]==6?cnt+1:0); 44 } 45 return ans; 46 } 47 int main() 48 { 49 //freopen("cin.in","r",stdin); 50 //freopen("cout.out","w",stdout); 51 T=read(); pre(); 52 while(T--) 53 { 54 ll S=read(),l=0,r=100000000000ll; 55 while(l+1<r) 56 { 57 ll mid=(l+r)/2; 58 if(getans(mid+1)>=S) r=mid; 59 else l=mid; 60 } 61 if(getans(r)==S) printf("%I64d\n",l); 62 else printf("%I64d\n",r); 63 } 64 return 0; 65 }