杭电60道DP问题总结:
DP是一个很有艺术的思想。看似简单的背后却隐藏着深刻的含义。
题目连接地址:http://acm.hdu.edu.cn/problemclass.php?id=516&page=1
学习算法最好的办法就是不断的练习。
本来想等把全部的DP问题做完了再发一个总帖,但是发现还是会有忘记,断断续续的做了一些,还是先总结一部分,之后的等考完试了再补充回来。
所有题目都AC过了,有自己写的,也有从网上参考学习的,拿出来与大家交流分享,我个人对某些题目的理解可能有误,如果有的话,还希望大家批评指正。
言归正传,一道一道题目来。
ProID 1003 Max Sum
题目意思是,给定一个数组,然后求这个数组中最大的连续子串之和,输出不仅需要输出最大值,还需要输出这个范围。
这算是最简单的DP问题之一了。
设 f[x] 为以 a[x]终止且包含 a[x] 的最大序列的和,有:f[1] = a[1];
f[x+1] = f[x] > 0 ? f[x] + a[x+1] : a[x+1],那么最大子序列的和就是 f[1] .. f[n] 中最大的一个。
这里一定要注意是以a[x]结尾的,就是所谓的无后效性,开始学习的时候不明白为什么一定要以a[x]结尾并且包含a[x]呢,注意DP是把大问题的规模缩小,现在我们假设f[x]为仅仅以a[x]结尾但不包括a[x]的最大子串之和,那么扩大问题的公式(就是所谓的递归方程式或者状态转移方程)如何写呢?
f[x+1]=?无法推导了,因为当考虑第x+1元素时,题中要求是连续子串,f[x]表示的前x个中的子串,可不一定包括第x个,虽然无后效性了,但是无法从前边的状态推导过来,即只能解决规模为1的问题。
一定记住,状态转移方程是从当前状态转到下一个状态时,增大问题规模,但是一定是与前一个状态而来的,切记。
回到问题上来,就可以理解为什么以a[x]结尾并且包含a[x]了。
至于找到区间就简单了,首先在结果集中找到最大值,注意f[x]表示包含第x个元素,但是最终的结果可不一定以最后一个元素结尾,所以还需要对f数组搜索,找到最大值,然后记录下最大值的位置,向前依次减去元素,等到结果为0时在记录下此时的下表,这样就出结果了。
参考代码如下:
#include<cstdio> #include<cstring> #include<cstdlib> int a[100001]; int dp[100001]; int main() { int T; scanf("%d",&T); for(int i=0;i<T;i++) { int n; scanf("%d",&n); /* 设 f[x] 为以 a[x] 终止且包含 a[x] 的最大序列的和,有:f[1] = a[1]; f[x+1] = f[x] > 0 ? f[x] + a[x+1] : a[x+1] 那么最大子序列的和就是 f[1] .. f[n] 中最大的一个。 */ memset(dp,0,sizeof(dp)); for(int j=0;j<n;j++) scanf("%d",&a[j]); dp[0]=a[0]; for(int k=1;k<n;k++) { if(dp[k-1]>0) dp[k]=dp[k-1]+a[k]; else dp[k]=a[k]; } int start=0; int max=dp[0]; for(int k=1;k<n;k++) { if(max<dp[k]) { start=k; max=dp[k]; } } int end=start; for(int k=end-1;k>=0;k--) { if(dp[k]>=0) end=k; else break; } printf("Case %d:\n",i+1); printf("%d %d %d\n",max,end+1,start+1); if(i!=T-1) printf("\n"); } return 0; }
这个题目还是有点难度的,我也是在网上查了些资料才大概搞明白的,树形DP问题。
大概意思是:有N个洞,M个士兵,每个洞中有a个bug,b个brain,每个士兵可以最多Kill掉20个bug,入口在洞口1处,问可以得到多少个brain。
这些洞是按照图的形式连接的,题中在输入数据的时候给出了图的边。不过根据题意,只有N-1条边,所以刚好是一个树,也就是所谓的树形DP,当然我是这样理解的,树形DP可不是这么简单,如果要深入理解的话,还是要去google一下资料(插一句,这方面的东西google出来的比baidu的多)。
回到题目,状态方程:value[a][b] = max(value[a][b],value[a][b - k] + value[j][k]),其中a、j之间有边相连。value[a][b]表示以a为根节点的子树,包含b个士兵,所能得到最大brain。 按a来分层进行动态规划。 这里的状态转移方程是根据与之相连的点所决定的,因为只能从当前节点走到与之相连的点。注释在代码中写的很清楚了。
代码如下:
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; struct Room { int bugs; int brains; }; Room rooms[101]; int value[101][101]; int matrix[101][101]; int visited[101]; int n,m; void dpTree(int u) { visited[u]=1; int r=(rooms[u].bugs+19)/20; //这个r表示当前这个房间里需要留几个士兵,即使是1个虫子也需要一个兵。 for(int i=m;i>=r;i--) value[u][i]=rooms[u].brains;//只要在这个房子里留够士兵,那么就可以获得全部的brains for(int v=1;v<=n;v++) { if(matrix[u][v] && !visited[v])//选择当前点相邻的点,并且此边没有访问过,注意只能往下走而不能走回去 { dpTree(v); for(int j=m;j>=r;j--) for(int k=1;k<=j-r;k++) value[u][j]=max(value[u][j],value[u][j-k]+value[v][k]); }//这个状态转移方程的意思是,第u个房间里留j个士兵所获得brains是由在第u个房间里留j-k个士兵,在与之相连的第v个房间里就k个士兵所获得的brains所决定的。一定记住了,当前的状态仅仅由与其相连的点的状态所决定!!! } } int main() { while(scanf("%d %d",&n,&m)!=EOF) { if(n==-1) break; memset(rooms,0,sizeof(rooms)); memset(matrix,0,sizeof(matrix)); memset(visited,0,sizeof(visited)); memset(value,0,sizeof(value)); for(int i=1;i<=n;i++) scanf("%d %d",&rooms[i].bugs,&rooms[i].brains); for(int i=1;i<n;i++) { int u,v; scanf("%d %d",&u,&v); matrix[u][v]=matrix[v][u]=1; } if(m==0) { printf("0\n"); continue; } dpTree(1); printf("%d\n",value[1][m]); } return 0; }
ProID 1024 Max Sum Plus Plus
给题目意思是,定由n个整数(可能为负整数)组成的序列e1,e2,…,en,以及一个正整数m,要求确定序列的m个不想交子段,使得这m个子段的总和达到最大。说实话,这个题目也是搞了好久才明白过来。
如果m=1,那么就是我们上边所说的第一道题目了,不过问题可不是那么简单的。分段之后可能原来不需要的元素变得可以使用了,所以得转换一下思路。
根据一般的思路而言,状态的转移也就是增大数据规模的过程,对于本题而言,就是不断考虑下一个元素的过程(也就是顺序DP),这里可以考虑使用模糊方程:
dp[i][j] =MAX(dp[?][?],dp[??][??])
套公式,dp[i][j]表示前j个元素分成i段的最大和(还记得我们第一题的内容么,是一个一维数组),那么考虑下一元素(注意这里不需要一定包含第j个元素了,因为有分段的存在)。
dp[i][j]=MAX(dp[i][j-1] , dp[i-1][k])+data[j] 其中i<k<=j。
表示第j个元素可以自成一段,或者拼接到第i段的最后一位。好了,问题解决了,不过回过头来看看,方程式中仅仅用到了当前状态和前一状态,所以不用开2维数组,根据题目要求,这个空间消耗比较大,只需要2个一维数组记录当前和原始状态就可以了。具体解释在代码中。
代码如下:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; int curr_best[1000010];//保存当前状态 int prev_best[1000010]; int max_sum,i,j; int n,m; int data[1000010]; #define MIN_SUM 0x80000000 int main() { while(scanf("%d %d",&m,&n)!=EOF) { for(int i=1;i<=n;i++) scanf("%d",&data[i]); memset(curr_best,0,sizeof(curr_best)); memset(prev_best,0,sizeof(prev_best)); int j=0; for(int i=1;i<=m;++i) { max_sum=MIN_SUM; for( j=i;j<=n;++j) { //curr b(i,*); //prev b(i-1,*); curr_best[j]=max(curr_best[j-1],prev_best[j-1])+data[j]; prev_best[j-1]=max_sum;//这两条语句不能写反了,这块我纠结了好久,解释一下,prev_best[j-1]表示的是上一个状态中i...j-1的最大值,max_sum更新之后表示的i...j的最大值,所以不能写反了 max_sum=max(max_sum,curr_best[j]); } prev_best[j-1]=max_sum;//prev_best[j-1]中始终保持着前一个状态的最大值,这个很重要 } printf("%d\n",max_sum); } return 0; }
ProID 1025 Constructing Roads In JGShining's Kingdom
题目意思是,两条线,每条线上各有n个点,表示n个城市,第一条表示rich 城市,第二天表示poor 城市,从第一条线连接到第二条线上,问最多能练几条,已知所有可连接的线段。
开始没搞明白,才发现就是个最长上升子序列问题。不过用一般的解法会超时,查阅资料后方得知原来需要DP+二分查找。说来惭愧,当时看明白了,可惜今天再拿出来瞅瞅又给忘了。其实最终的代码很简单,但往往是越简单的代码蕴含的思想越是复杂牛逼。下面的解释摘自Lce_Crazy的博客。
假定存在一个序列d[1...9]=2 1 5 3 6 4 8 9 7,可以看出LIS长度为5。现在开始一步一步的找出来最终的结果。
定义序列B,令i=1...9来逐个考察。此外用变量len来记录现在最长算到了多少了。
首先,把d[1]放到B里,即B[1]=d[1],就是说长度为1的LIS的最小末尾是2,这时len=1,然后把d[2]有序的放到B里,令B[1]=1(d[2]),就是说长度为1的LIS最小末尾是1,d[1]=2已经没有用了,这是len=1不变。接着d[3]=5,5>1,所以令B[1+1]=d[3]=5,就是说长度为2的LIS的最小末尾是5,此时B[1...2]=1,5,len=2。
再来,d[4]=3,它正好在1和5之间,当然放在1的位置上不合适,因为1<3,因此长度为2的最小末尾应该是3,淘汰掉5,这时候B[1...2]=1,3,len=2,继续d[5]=6,它在3后边,所以B[3]=4,B[1...3]=1,3,6,len=3。
第6个,d[6]=4,它加在3和6之间,所以我们把6换掉,这样B[1...3]=1,3,4,len=3。
第7个,d[7]=8,8>4,所以B[4]=8,B[1...4]=1,3,4,8,len=4。
第8个,d[8]=9,B[1...5]=1,3,4,8,9,len=5。
最后一个,d[9]=7,所以B[1...5]=1,3,4,7,9,len=5。
注意这里的B并不是LIS,而是对应长度的LIS的最小末尾。
现在可以发现B插入数据是有序的,而且进行替换而不需要移动,也就是说可以使用二分查找,时间复杂度就降下来了。
PS:注意了,这种算法只能得到最终的数据个数,但是如果需要得到具体的内容就不行了。
提示一下,看网上说首先要排序,其实仔细看看题目的说明,它的边的输入是有序的,所以根本不需要进行排序。
代码如下:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; int num[50001]; int ans[50001]; int main() { int n,k=1; int len,low,heigh,mid; int tmp1,tmp2; while(scanf("%d",&n)!=EOF) { for(int i=0;i<n;i++) { scanf("%d %d",&tmp1,&tmp2); num[tmp1]=tmp2; } memset(ans,0,sizeof(ans)); ans[1]=num[1]; len=1; for(int i=2;i<=n;i++) { low=1; heigh=len; while(low<=heigh) { mid=(low+heigh)/2; if(ans[mid]<num[i]) low=mid+1; else heigh=mid-1; } ans[low]=num[i]; if(low>len) len++; } printf("Case %d:\n",k); if(len==1) printf("My king, at most 1 road can be built."); else printf("My king, at most %d roads can be built.",len); printf("\n\n"); k++; } return 0; }
ProID 1058 Humble Number
先呵呵下,这道题被各种吐槽,英文不过关啊。
题目是让找出第N个Humble Number,定义是除1外所有仅有2,3,5,7因子的数。
不多少了,这个题目或许可以用DP来解,但是貌似完全没有必要。不解释了。
代码如下:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; int n; typedef long long LL; LL data[5843]; void init() { int i=1,j=1,m=1,n=1; int index=1; data[index]=1; while(1) { if(index==5843) break; LL tmp1=min(data[i]*2,data[j]*3); LL tmp2=min(data[m]*5,data[n]*7); LL res=min(tmp1,tmp2); if(res== data[i]*2) i++; else if(res== data[j]*3) j++; else if(res== data[m]*5) m++; else if(res== data[n]*7) n++; if(res!= data[index]) data[++index]=res; } } int main() { init(); while(scanf("%d",&n)!=EOF,n) { if(n%10==1 && n%100!=11) printf("The %dst humble number is %lld.\n",n,data[n]); else if(n%10==2 && n%100!=12) printf("The %dnd humble number is %lld.\n",n,data[n]); else if(n%10==3 && n%100!=13) printf("The %drd humble number is %lld.\n",n,data[n]); else printf("The %dth humble number is %lld.\n",n,data[n]); } return 0; }
题目意思是,有一堆钻石,有6种不同的价值(1...6),现在告诉你每种的数量,问你能不能把这堆钻石平均按总的价值平均分开。
之前有做过9度上的一道题目,是把一堆球平均分成2堆,大概算法是:
求出所有总重量<= N / 2的可能性,如F[w]=true,表示可以累积1个或若干个球得到w的总重量。最后找出最接近N/2的即可。时间复杂度O(n*N),n为球的总数量,N为球的总质量。
这里有不同的地方,首先每个球的个数不定,而且我用上述方法的时候超时了,所以考虑别的方法。但有一点是一样了,只要能够得到刚好总价值一半的值,那么就可以平均分,怎么得呢,当然是背包问题了,不过不是01背包,而是多重背包问题,只要以总价值的一半为背包重量,那么只要能装满,就可以平均分。
代码是参考背包九讲中的公式:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; int data[7]; int dp[500001]; int m; void ZeroOnePack(int value,int weight) { for(int i=m;i>=value;i--) dp[i]=max(dp[i-value]+weight,dp[i]); } void CompletePack(int value,int weight) { for(int i=value;i<=m;i++) dp[i]=max(dp[i],dp[i-value]+weight); } void MultiPack(int value,int weight,int amount) { if(value*amount>=m) { CompletePack(value,weight); return ; } for(int k=1;k<amount;k*=2) { ZeroOnePack(k*value,k*weight); amount-=k; } ZeroOnePack(amount*value,amount*weight); } int main() { int count=1; while(scanf("%d",&data[1])!=EOF) { for(int i=2;i<=6;i++) scanf("%d",&data[i]); bool out=true; int sum=0; for(int i=1;i<=6;i++) { sum+=data[i]*i; if(data[i]) out=false; } if(out) break; printf("Collection #%d:\n",count++); if((sum&1) ==1) { printf("Can't be divided.\n"); printf("\n"); continue; } m=sum/2; int value=0; memset(dp,0,sizeof(dp)); for(int i=1;i<=6;i++) if(data[i]) MultiPack(i,i,data[i]); if(dp[m]==m) printf("Can be divided.\n"); else printf("Can't be divided.\n"); printf("\n"); } return 0; }
题目大意是,一只猴子,想吃香蕉,给他一堆垫子,他把垫子堆起来自己站上去好能够到香蕉,堆垫子的规则是下边的面积一定要大于上边的面积。
这个题目依然是最长上升子序列问题,只不过比较的规则是面积而已。
PS:每种垫子可以使用多个,垫子有长宽高,可以以多种样子摆放。
不多解释了,代码如下:
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; struct _data { int x,y,z,area; }; _data data[200]; bool com(_data d1,_data d2) { return d1.area<d2.area; } int n; int dp[200]; int main() { int count=1; while(scanf("%d",&n)!=EOF&&n) { int index=0; for(int i=0;i<n;i++) { int x,y,z; scanf("%d %d %d",&x,&y,&z); data[index].x=x;data[index].y=y;data[index].z=z;data[index++].area=x*y; data[index].x=y;data[index].y=x;data[index].z=z;data[index++].area=x*y; data[index].x=y;data[index].y=z;data[index].z=x;data[index++].area=z*y; data[index].x=z;data[index].y=y;data[index].z=x;data[index++].area=z*y; data[index].x=x;data[index].y=z;data[index].z=y;data[index++].area=x*z; data[index].x=z;data[index].y=x;data[index].z=y;data[index++].area=x*z; } sort(data,data+index,com); memset(dp,0,sizeof(dp)); dp[0]=data[0].z; for(int i=1;i<index;i++) { int temp=0; for(int j=0;j<i;j++) if(data[i].x>data[j].x && data[i].y>data[j].y&&temp<dp[j]) temp=dp[j]; dp[i]=temp+data[i].z; } int ans=dp[0]; for(int i=1;i<index;i++) ans=max(ans,dp[i]); printf("Case %d: maximum height = %d\n",count++,ans); } return 0; }
ProID 1074 Doing Homework
题目大意是,给定一些课程,包括截止期和扣分,问怎样编排课程是得扣分最少。
看了半天没有思路,只好google,用的是所谓的压缩DP,用一位表示一个状态,也算是个好题,至少学到了些基本的方法。
下面的大部分东西摘自GentleH的博客,代码里我增加了注释,应该还是比较容易理解的。
状态压缩DP
因为只有15个课程,用15位的二进制表示课程的完成状态,1表示完成,0表示未完。
假定有4门课程,1100表示的是第4门和第3门课是完成的,也就是说可以用12这个十进制数来表示完成了第4和第3门课这个状态。那么15门课程,如果表示全部都完成了,那么只需用32767来表示。
二进制操作:
(1)i & j:j为第j门课,则这个操作表示的是i这个状态是否包含了j,也就是得到i和j的相同之处。比如1011 & 0010 = 0010(!=0),说明i是包含j的。相同部分就是j。
(2)i ^ j: 对于i和j两个不同的状态,该操作的结果是得到i和j的不同之处。比如1011 ^ 0010 = 1001。说明了i这个状态除了j之外,还有第4门和第1门是完成的。这跟&操作恰好相反。
PS: i | j:将i和j两个状态合并,比如1011 & 0100 = 1111;
状态方程比较复杂,我解释一下。
dp[i]中有3个字段,pre,res,cost,分别表示上一个状态,需要扣的分,以及达到当前状态需要花费的时间。
这个下表i范围是0...1<<n,用二进制的每一位表示一个作业。
在当前状态i中,如果包含了课程cur,那么这个状态就是由这个课程转换而来的,规则是
状态i中的cost=状态 i^cur(出cur课程以外所有课程)的cost+完成课程cur所需要的时间。
如果到达状态i时所花费的总时间不超过课程cur的deadline,那么就不会增加扣分。否则需要记录这个扣分,咋那么总的扣分是状态i^cur的扣分+完成当前课程所扣的分(有就加,没有就不加)。现在需要确定是否状态转换,是否转换时候扣的分的多少而决定的。如果当前状态的扣分>由cur转换过来的扣分,那么就更新状态,即当前状态是由cur转换过来的。
代码如下:
#include<iostream> #include<string> #include<cstring> #include<cstdio> using namespace std; struct Work { char name[105]; int deadline; int cost; }work[20]; struct DP { int pre; int res; int cost; }dp[40005]; int N; void out(int state) { if(state>0) { out(dp[state].pre); int t=dp[state].pre; int ans=t ^ state;//找出当前状态和上一状态的不同之处 //也就是当前的课程 int idx=0; //找出该十进制数在二进制表示下,1出现在什么位置,因为1代表完成了哪个课程。 while(ans>0) { idx++; ans>>=1; } printf("%s\n",work[idx].name); } } int main() { int T; scanf("%d",&T); while(T--) { scanf("%d",&N); for(int i=1;i<=N;i++) scanf("%s %d %d",&work[i].name,&work[i].deadline,&work[i].cost); int Max=(1<<N )-1; dp[0].cost=0; dp[0].pre=-1; dp[0].res=0; for(int i=1;i<=Max;i++) { dp[i].cost=0; dp[i].pre=-1; dp[i].res=0xfffffff; for(int j=0;j<N;j++) { int cur=1<<j; if(i & cur) //包含了该课程 {//状态,pre是上一个状态,res是需要扣的分,cost是达到当前状态需要花费的时间 dp[i].cost=dp[i^cur].cost+work[j+1].cost; int reduce=dp[i].cost-work[j+1].deadline; if(reduce<0) reduce=0; reduce += dp[i^cur].res; //因为题目中的课程输入已经保证是字典序,所以==的时候一样更新 if(dp[i].res>=reduce) { dp[i].res=reduce; dp[i].pre=i^cur; } } } } printf("%d\n",dp[Max].res); out(Max); } return 0; }
ProID 1078 FatMouse and Chese
要满足 x1==x2&& |y1-y2|<=k 或者 y1==y2&&|x1-x2|<=k。
对于路径问题,一般都是深度搜索,所以我们也用DFS方法。
解释一下状态转移方程,dp[i][j]表示以第i行第j列个网格为起点所能得到的最大值,注意了因为是深度搜索,是不断向下走的,所以状态的转移是以下边的状态为基础的,而不是以前边的状态为基础,因此就不是这个意思:dp[i][j]表示从0,0到i,j的最大值,这是绝对错误的。
代码如下:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; int data[101][101]; int dp[101][101];//dp[i][j]表示从位置(i,j)为起点时能吃到最多的 int dir[4][2]={{1,0},{-1,0},{0,1},{0,-1}}; int n,k; int Dfs(int x,int y) { if(dp[x][y]>0)//如果已经有值了,可以直接返回 return dp[x][y]; int Max=0; for(int i=0;i<4;i++)//搜四个方向 for(int j=1;j<=k;j++)//一次可以跳1至k步 { int tx=x+dir[i][0]*j; int ty=y+dir[i][1]*j; //必须跳的位置的奶酪比当前所站的位置多 if(tx>=0 && tx<n && ty>=0 && ty<n && data[x][y]<data[tx][ty]) { int temp=Dfs(tx,ty); Max=max(Max,temp); } } dp[x][y]=Max+data[x][y]; return dp[x][y]; } int main() { while(scanf("%d %d",&n,&k)!=EOF) { if(n==-1 && k==-1) break; for(int i=0;i<n;i++) for(int j=0;j<n;j++) scanf("%d",&data[i][j]); memset(dp,0,sizeof(dp)); Dfs(0,0); printf("%d\n",dp[0][0]); } return 0; }
ProID 1080 Human Gene Function
代码如下:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <map> #include <string> using namespace std; map<string,int>data; int dp[110][110],match[110][110]; char s1[110],s2[110]; int T; void init() { match['A']['A']=match['C']['C']=match['G']['G']=match['T']['T']=5; match['A']['C']=match['C']['A']=match['A']['T']=match['T']['A']=-1; match[' ']['T']=match['T'][' ']=-1; match['A']['G']=match['G']['A']=match['C']['T']=match['T']['C']=-2; match['G']['T']=match['T']['G']=match['G'][' ']=match[' ']['G']=-2; match['A'][' ']=match[' ']['A']=match['C']['G']=match['G']['C']=-3; match['C'][' ']=match[' ']['C']=-4; } int main() { init(); while(scanf("%d",&T)!=EOF) { int n1,n2; while(T--) { scanf("%d %s",&n1,s1+1); scanf("%d %s",&n2,s2+1); memset(dp,0,sizeof(dp)); //初始化时需要特别注意 for(int i=1;i<=n1;i++) { dp[i][0]=dp[i-1][0]+match[s1[i]][' ']; } for(int i=1;i<=n2;i++) { dp[0][i]=dp[0][i-1]+match[' '][s2[i]]; } for(int i=1;i<=n1;i++) for(int j=1;j<=n2;j++) { int tmp1=dp[i-1][j-1]+match[s1[i]][s2[j]]; int tmp2=dp[i-1][j]+match[s1[i]][' ']; int tmp3=dp[i][j-1]+match[' '][s2[j]]; int res=max(tmp1,max(tmp2,tmp3)); dp[i][j]=res; } printf("%d\n",dp[n1][n2]); } } return 0 ; }
好了,时间也不早了,先整理这个前10到题目吧,话说这10道题目也是有难度的,虽然是第二次做,但是还是出了不少问题,所以即使现在糊了,可能过几天又给忘了,总之一句话,需要勤加练习才是。
大家晚安!