一、数字金字塔,太经典了,可以自顶向下算,也可以自底向上算
二、子序列问题(线性DP)
- 最长不下降子序列,很经典,注意分析题目,很多题目做点细节然后本质就是求最长不下降子序列
合唱队形:从两边求最长不下降子序列,然后遍历每一个点,分析它的两边的情况,选择最大的
int n; int a[201],c[201];
//本身,前驱 int b[201];
//得到的结果序列 //第一种做法,复杂度O(N^2) int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i];b[i]=1;c[i]=0; } int l,k; for(int i=n-1;i>=1;i--){ l=0,k=0; for(int j=i+1;j<=n;j++){ if(a[j]>a[i]&&b[j]>l) { //选择大于它的且现在长度最大的 l=b[j]; k=j; } } if(l>0){ b[i]=l+1; c[i]=k; } } k=1; for(int i=1;i<=n;i++) if(b[i]>b[k]) k=i; cout<<"max="<<b[k]<<endl; //路径的输出 while(k!=0){ cout<<" "<<a[k]; k=c[k]; } return 0; }
第二种算法:复杂度O(Nlog2N)
两种操作,先与已经选择好的序列最后一位进行比较,如果大于最后一位,那么就直接放进去,len++;
如果比最后一位小,那么就在已经选择好的序列里面找到比它大的第一个数,然后替换,因为这样能够保障插入更多的数
//利用有序队列优化,f[i]=max(f[j]+1),j<i且a[j]<=a[i],而且f[j]要尽可能地大!!! int n; int a[maxn]; //本事 int d[maxn]; //得到的结果序列 int pre[maxn]; //用来输出路径 int main(){ cin>>n; int len=1; for(int i=1;i<=n;i++) cin>>a[i]; d[1]=a[1];pre[1]=1; for(int i=2;i<=n;i++){ if(d[len]<=a[i]) { d[++len]=a[i];pre[i]=len; } else{ int j=upper_bound(d+1,d+1+len,a[i])-d; //返回第一个大于a[i]的坐标 d[j]=a[i]; //否则就找到位置替换掉 pre[i]=j; } } stack<int> st; for(int i=n,j=len;i>=1;i--){ if(pre[i]==j){ st.push(a[i]); --j; } if(j==0) break; } cout<<len<<endl; while(!st.empty()){ cout<<st.top()<<" "; st.pop(); } return 0; }
- 最长公共子序列,最简单的了
cin>>a>>b; int ma=strlen(a); int mb=strlen(b); for(int i=1;i<=ma;i++){ for(int j=1;j<=mb;j++){ if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1]+1;//注意这里是a[i-1]==b[i-1] else f[i][j]=max(f[i-1][j],f[i][j-1]); //配合前面i=1,j=1 } } cout<<f[ma][mb]<<endl;
- 最长公共上升子序列
int a[501],b[501]; int t[501][501]={0}; //用来记录的 int s[501]; //临时存储 int n,m; int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; cin>>m; for(int i=1;i<=m;i++) cin>>b[i]; // a[0]=b[0]=-999999;//预处理边界值 //以b为大循环,遍历a与之比较 for(int i=1;i<=m;i++){ memset(s,0,sizeof(s));//初始化s for(int j=1;j<=n;j++){ if(b[i]>a[j]&&s[0]<t[j][0]){//符合局部上升且之前存储过公共上升子序列 memcpy(s,t[j],sizeof(t[j])); //就保存到s中(调出之前存储的公共上升子序列) } if(b[i]==a[j]){ memcpy(t[j],s,sizeof(s));//将s复制给t,存储当前情况下子序列 t[j][++t[j][0]]=a[j];//接上子序列并计算长度+1 } } } int ans=0;//找出最大长度 for(int i=1;i<=n;i++) if(ans<t[i][0]) ans=t[i][0]; cout<<ans<<endl; for(int i=1;i<=n;i++) { if(t[i][0]==ans){ for(int j=1;j<=ans;j++) cout<<t[i][j]<<" "; break; } } return 0; }
- 最长回文子串
动态规划的做法:最简单O(N^2)
字符串hash+二分的算法(O(nlogn))
最优秀的Manacher算法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//字符串hash+二分 //写写思路:首先可以先到把字符串反转,计算两个字符串的hash值,然后进行比较,看最大的半径(以分界点为中心)在哪里,但是如果单纯枚举半径 //就会超时,所以二分回文半径 //但是有需要区分:回文串长度是奇数还是偶数(因为区间不同,所以分别计算:回文长度为偶数和回文长度为奇数的情况,计算最大的回文半径 //《算法笔记》第45页 long long pw[maxn],h1[maxn],h2[maxn]; void inti(int len){ pw[0]=1; for(int i=1;i<=len;i++) pw[i]=(P*pw[i-1])%MOD; //计算每一位进制数 } void gethash(string &s,long long pe[]){ //计算字符串的hash数组 pe[0]=s[0]-'a'; for(int i=1;i<s.length();i++){ pe[i]=(pe[i-1]*P+s[i]-'a')%MOD; } } long long jssubstrhash(long long h[],int i,int j){ //计算子串的hash值 if(i==0) return h[j]; //如果以0开头,就直接返回(已经计算过了) //公式:h[i..j]=((h[j]-h[i-1]*p^(j-i+1))%MOD+MOD)%MOD else return ((h[j]-h[i-1]*pw[j-i+1])%MOD+MOD)%MOD; } //回文半径上下限为l,r,分界点为i,字符串串长为len,判断是不是整数iseven int getsearch(int l,int r,int len,int i,int iseven){ while(l<r){ int mid=(l+r)/2; int h1l=i-mid+iseven,h1r=i; int h2l=len-1-(i+mid),h2r=len-1-(i+iseven); int hash1=jssubstrhash(h1,h1l,h1r); int hash2=jssubstrhash(h2,h2l,h2r); if(hash1!=hash2) r=mid; //不匹配:回文半径太大了,减小一点 else l=mid+1; //相等的话说明还可能可以扩充 } return l-1; //返回最大回文半径 } string str; void findmaxbanjing(){ getline(cin,str); inti(str.length()); gethash(str,h1); //计算原来的串的hash reverse(str.begin(),str.end()); //反转这个串 gethash(str,h2); //区分奇数回文长度和偶数回文长度 int ans=0; for(int i=0;i<str.length();i++){ int maxlen=min(i,(int)str.length()-1-i)+1; //最大的回文半径(上限)--左右长度最小值+1 //注意str.length()前面要加(int),不要会出现错误!! int k=getsearch(0,maxlen,str.length(),i,0); //iseven=0; ans=max(ans,2*k+1); } //偶数 for(int i=0;i<str.length();i++){ int maxlen=min(i+1,(int)str.length()-1-i)+1; //注意最大的回文长度 (左长为i+1)!!!不知道why //注意str.length()前面要加(int),不要会出现错误!! int k=getsearch(0,maxlen,str.length(),i,1); ans=max(ans,2*k); } cout<<"max huiwen substr leghth is "<<ans<<endl; }
//最长回文子串 const int maxn=1001; char a1[maxn]; int dp[maxn][maxn];//其实这是个类似于bool数组的作用,只是用来判断是不是回文串 int findmaxhuiwen(){ gets(a1); int ans=1; int len=strlen(a1); for(int i=0;i<len;i++){ dp[i][i]=1; if(i+1<len){ if(a1[i]==a1[i+1]) { dp[i][i+1]=1; ans=2; } } }//初始化 for(int l=3;l<=len;l++){ //以长度来循环 for(int i=0;i+l-1<len;i++){ //左边端点 int j=i+l-1; //右边端点 //里面不需要循环了,只有判断一次就够了 if(a1[i]==a1[j]&&dp[i+1][j-1]) { dp[i][j]=1; ans=l; //l为长度 } } } return ans; }
- 最大连续子序列和
int a2[maxn],dp1[maxn]; //dp[i]表示以i为结尾的最大连续子序列和 //最大连续子序列和 int findmaxsum(){ int n; cin>>n; for(int i=0;i<n;i++){ cin>>a2[i]; } dp1[0]=a2[0]; for(int i=1;i<n;i++){ dp1[i]=max(dp1[i-1]+a2[i],a2[i]); //前一个为负数 } int maxx=-99999; for(int i=0;i<n;i++) maxx=max(maxx,dp1[i]); return maxx; }
涉及两个序列比较的:最长公共子序列、最长公共递增子序列
三、拦截导弹,也太经典了
一个最多能拦多少,就是最长不下降子序列
cin>>n; for(int i=1;i<=n;i++) {cin>>a[i];b[i]=1; } int l=0,m=0,nn=1,k; h[1]=a[1]; for(int i=n-1;i>=1;i--){ l=0; for(int j=i+1;j<=n;j++){ if(a[i]>a[j]&&b[j]>l) l=b[j]; } if(l>0) { b[i]=l+1; } //if(b[i]>m) m=b[i]; //最多能拦多少 } for(int i=1;i<=n;i++) m=max(m,b[i]); for(int i=2;i<=n;i++){ k=0; for(int j=1;j<=nn;j++){ if(h[j]>=a[i]) {//能拦 if(k==0) k=j; else if(h[j]<h[k]) k=j; //还有更低的导弹高度 } } if(k==0) {nn++; h[nn]=a[i];}//增加导弹数量 else h[k]=a[i]; //否则更新导弹高度 } cout<<m<<" "<<nn<<endl;
四、机器分配,这个输出比较特别,状态转移方程明白意思
int n,m; int f[11][16]; int a[11][16]; //记住这个输出!!!很特别 int show(int x,int y){ //x位公司数,y位剩下的台数 int k; if(x==0 ) return 0; for(k=0;k<=y;k++){ if(f[n][m]==f[x-1][k]+a[x][y-k]) { //前面分了k台,即剩下k台全部是前面的 f[n][m]-=a[x][y-k]; show(x-1,k); cout<<x<<" "<<y-k<<endl; //顺序 break; } } } int main(){ cin>>n>>m; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>a[i][j]; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ for(int k=0;k<=j;k++) f[i][j]=max(f[i][j],f[i-1][k]+a[i][j-k]); //状态转移方程含义 //前i个公司分配j台能够得到的最大值 前i个分k台,第i个分配j-k台 }//k为中间控制变量 } cout<<f[n][m]<<endl; show(n,m); return 0; }
五、背包问题
- 01背包:每个东西只有一份
cin>>m>>n; for(int i=1;i<=n;i++) cin>>w[i]>>c[i]; for(int i=1;i<=n;i++){ for(int j=m;j>=w[i];j--){ //逆序 f[j]=max(f[j-w[i]]+c[i],f[j]); //这里是J } } cout<<f[m]<<endl;
- 完全背包:每个东西可以取无限份
cin>>m>>n; for(int i=1;i<=n;i++) cin>>w[i]>>c[i]; for(int i=1;i<=n;i++){//顺序 for(int j=w[i];j<=m;j++) if(f[j]<f[j-w[i]]+c[i]) f[j]=f[j-w[i]]+c[i]; } cout<<"max="<<f[m]<<endl;
- 混合背包:有的物品有多个有的物品只有1个,分别进行处理,逆序or正序,注意选择多个的时候循环次序,物品数量在外层,背包重量在内层
cin>>m>>n; for(int i=1;i<=n;i++) cin>>w[i]>>c[i]>>s[i]; for(int i=1;i<=n;i++){ if(s[i]==0){ //完全背包,顺序 for(int j=w[i];j<=m;j++){ f[j]=max(f[j],f[j-w[i]]+c[i]); } } else {//01背包和多重背包,逆序 for(int j=1;j<=s[i];j++) //注意顺序,先是物品个数,再是剩余重量 for(int k=m;k>=w[i];k--) f[k]=max(f[k],f[k-w[i]]+c[i]); //这里没有j噢 } } cout<<f[m]<<endl;
- 多重背包,可以通过二进制处理降低复杂度,原理是每个数都可以表示位2进制相加的形式,所以这样就把它处理为了01背包问题
-
int x,y,n1=0,s,t; for(int i=1;i<=n;i++){ cin>>x>>y>>s; t=1; while(s>=t){ w[++n1]=t*x; //重量 c[n1]=t*y; //价值 s-=t; t*=2; } w[++n1]=s*x; c[n1]=s*y; } //接下来就是01背包问题 for(int i=1;i<=n1;i++){ //注意数量 for(int j=m;j>=w[i];j--){ f[j]=max(f[j],f[j-w[i]]+c[i]); } } cout<<f[m]<<endl;
- 二维背包问题:eg.潜水员
cin>>m>>n>>k; memset(f,127,sizeof(f));//赋值为一个很大的数,因为是要求最小的 f[0][0] =0; //f[0][0]要初始化 for(int i=1;i<=k;i++) cin>>mm[i]>>nn[i]>>w[i]; for(int i=1;i<=k;i++){ for(int j=m;j>=0;j--){//都是01背包 费用1 for(int k=n;k>=0;k--){ //费用2 int t1=j+mm[i]; int t2=k+nn[i]; //注意写法 ,不能在外面直接判断 if(t1>m) t1=m; if(t2>n) t2=n; if(f[t1][t2]>f[j][k]+w[i]) f[t1][t2]=f[j][k]+w[i]; } } } cout<<f[m][n]<<endl;
- 分组背包,背包分组,注意循环的顺序,最外层是组数,中间层是剩余的体积,最里层是这个组里面的物品序号
int p=0; cin>>v>>n>>k; for(int i=1;i<=n;i++){ cin>>w[i]>>c[i]>>p; g[p][++g[p][0]]=i; //这种写法,合并了两个功能 } for(int i=1;i<=k;i++){ //分的组数 for(int j=v;j>=0;j--){ //第二层是体积 for(int k=1;k<=g[i][0];k++){ //这个组的物品序号 if(j>=w[g[i][k]]){ //注意是>=,不然结果不正确 int temp=g[i][k]; if(f[j]<f[j-w[temp]]+c[temp]) f[j]=f[j-w[temp]]+c[temp]; } } } } cout<<f[v]<<endl;
六、货币系统,两种写法,与背包问题不同的是,这里是求的方案总数,所以直接加就好了,不用比较
cin>>n>>m; //这里是逆序的写法: 01背包 for(int i=1;i<=n;i++) cin>>a[i]; f[0]=1; //初始化 for(int i=1;i<=n;i++){ for(int j=m;j>=a[i];j--){ for(int k=1;k<=j/a[i];k++){ //注意这里有个数隐形限制 ,而且注意除的是J f[j]=f[j]+f[j-k*a[i]]; } } } cout<<f[m]<<endl; //下面是顺序的写法:完全背包 cin>>n>>m; for(int i=1;i<=n;i++) cin>>a[i]; f[0]=1; for(int i=1;i<=n;i++){ for(int j=a[i];j<=m;j++){ f[j]+=f[j-a[i]]; } } cout<<f[m];
七、数字组合,与货币系统很像,但是这个是01背包,那个是完全背包
//这道题和货币系统那道题很相似,都是求方案总数 //但是这道题是01背包 //货币系统是完全背包 int main(){ cin>>n>>t; for(int i=1;i<=n;i++) cin>>a[i]; f[0]=1; for(int i=1;i<=n;i++){ for(int j=t;j>=a[i];j--){ f[j]+=f[j-a[i]]; } } cout<<f[t]<<endl;
八、开餐馆
其实思路蛮简单,直接遍历每一个点,然后从开头到结尾,每个距离它有k的餐馆都加进去,然后选择最大的就可以了
int t; cin>>t; while(t--) { int n,k; cin>>n>>k; for(int i=1;i<=n;i++) cin>>w[i]; for(int i=1;i<=n;i++) { cin>>c[i]; f[i]=c[i]; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(w[i]-w[j]>k) f[i]=max(f[i],f[j]+c[i]); int maxx=-INF; for(int i=1;i<=n;i++) maxx=max(f[i],maxx); cout<<maxx<<endl; }
九、合并石子,注意这个合并石子是只能合并相邻的,如果随意的话就是堆(小根堆)
cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; s[i]=s[i-1]+a[i]; } memset(f,127/3,sizeof(f)); for(int i=1;i<=n;i++) f[i][i]=0; //赋一个极大的值但是相加不会溢出 for(int i=n-1;i>=1;i--){ for(int j=i+1;j<=n;j++){ for(int k=i;k<=j-1;k++){ if(f[i][j]>f[i][k]+f[k+1][j]+s[j]-s[i-1]) //把i--k和k+1--j这两堆合并起来,原本的得分加上现在的新增的得分 f[i][j]=f[i][k]+f[k+1][j]+s[j]-s[i-1]; } } } cout<<f[1][n]<<endl;
十、乘积最大,注意细节,以及循环的下标和循环的顺序
int n,m; long long s; //把字符串转化为数组存储。方便转化 long long f[11][7],a[11][11]; //注意这个也要用Longlong存储 int main(){ cin>>n>>m; cin>>s; for(int i=n;i>=1;i--){ a[i][i]=s%10; s/=10; } for(int i=2;i<=n;i++){ for(int j=i-1;j>=1;j--){ a[j][i]=a[j][i-1]*10+a[i][i]; } } //以上为处理 a数组的含义 for(int i=1;i<=n;i++) f[i][0]=a[1][i]; //前i位没有乘号,就是本身 //预处理:不加乘号的情况 for(int i=1;i<=m;i++) //这里的循环是针对乘号个数的 for(int j=i+1;j<=n;j++) //可以插入的 for(int k=i;k<j;k++) { //寻找合适的插入位置 ,不能少于乘号数,也不能大于j f[j][i]=max(f[j][i],f[k][i-1]*a[k+1][j]); } cout<<f[n][m]<<endl; return 0; }
十一、编辑距离。注意这个与公共子序列的区别,那个是上面+1
设A和B是两个字符串。我们要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有三种:
1、删除一个字符;
2、插入一个字符;
3、将一个字符改为另一个字符。
对任意的两个字符串A和B,计算出将字符串A变换为字符串B所用的最少字符操作次数。
cin>>a>>b; int la=strlen(a); int lb=strlen(b); for(int i=1;i<=la;i++) f[i][0]=i; for(int i=1;i<=lb;i++) f[0][i]=i; for(int i=1;i<=la;i++) //开头写错了!应该是i=1 for(int j=1;j<=lb;j++){ if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1]; else f[i][j]=min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1; } cout<<f[la][lb]<<endl;
十二、方格取数,进行四重循环,弄清如果是同一个地方的话,就只取一次
cin>>n; int x,y,z; while(cin>>x>>y>>z,x+y+z){ map[x][y]=z; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) for(int h=1;h<=n;h++) for(int k=1;k<=n;k++){ int temp1=max(f[i-1][j][h-1][k],f[i-1][j][h][k-1]); //下下 下右 int temp2=max(f[i][j-1][h-1][k],f[i][j-1][h][k-1]); //右下 右右 if(i!=h&&j!=k) f[i][j][h][k]=max(temp1,temp2)+map[i][j]+map[h][k]; else f[i][j][h][k]=max(temp1,temp2)+map[i][j]; } cout<<f[n][n][n][n]<<endl;
十三、复制书稿
现在要把m本有顺序的书分给k个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三和第四本书给同一个人抄写。
现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。
【输入】
第一行两个整数m,k;(k≤m≤500)
第二行m个整数,第i个整数表示第i本书的页数。
【输出】
共k行,每行两个整数,第i行表示第i个人抄写的书的起始编号和终止编号。k行的起始编号应该从小到大排列,如果有多解,则尽可能让前面的人少抄写。
这个输出的函数很special
int m,k; int a[501],f[501][501],d[501];//书的页数,结果,前几本书的总页数 void print(int x,int y){//分配抄书策略 ,贪心,逆序 if(y==0) return ; if(y==1) cout<<"1 "<<x<<endl; //x是书,是可变的,y是人,只有循环没有变化 int t=x,xx=a[x]; while(xx+a[t-1]<=f[k][m]){ xx+=a[t-1]; t--; } print(t-1,y-1); cout<<t<<" "<<x<<endl; //t是变化的i,而i是进来时的变量 return; } int main(){ cin>>m>>k; memset(f,100000,sizeof(f)); for(int i=1;i<=m;i++){ cin>>a[i]; d[i]=d[i-1]+a[i]; f[1][i]=d[i]; //前几个个人抄几本书 } for(int i=2;i<=k;i++){ //人:阶段 for(int j=1;j<=m;j++){ //书:状态 for(int k=1;k<=j-1;k++){ if(max(f[i-1][k],d[j]-d[k])<f[i][j]) f[i][j]=max(f[i-1][k],d[j]-d[k]);//前i-1个人抄k本书,第i个人抄d[j]-d[k]本书 取max是因为求的是时间 } } } print(m,k); //输出 return 0; }
十四、橱窗布置
这道题要注意很多细节,比如路径的保存,以及循环中下标的设置
//橱窗布置 int a[101][101],b[101][101];//分别是美学值,前i朵花放在前j个花瓶 int c[101][101],d[101];//存储路径 int f,v; int main(){ cin>>f>>v; for(int i=1;i<=f;i++) for(int j=1;j<=v;j++){ cin>>a[i][j]; } memset(b,128,sizeof(b));//将数组b初始化为一个很小的数 for(int i=1;i<=v-f+1;i++) b[1][i]=a[1][i];//第一束花放在第i个瓶子里=第一束花放在第一个瓶子里(含义是一样的) //注意上限是v-f+1 for(int i=2;i<=f;i++){//枚举花 for(int j=i;j<=v-f+i;j++){//枚举花瓶 (注意上限是v-f+i) for(int k=i-1;k<=j-1;k++){//枚举中间位置 if(b[i][j]<b[i-1][k]+a[i][j]){ b[i][j]=b[i-1][k]+a[i][j];//如果前i-1束花放在前k个花瓶里再加上i放j上的美学值要大于前i束花放在前j个花瓶 c[i][j]=k;//存储路径 } } } } int k;//存放最后一束花的位置 int maxx=-999999; for(int i=f;i<=v;i++){ if(maxx<b[f][i]){ maxx=b[f][i]; k=i;//存放最后一束花的位置 } } cout<<maxx<<endl; //!!!!!!! //下面是输出 for(int i=1;i<=f;i++){ d[i]=k; k=c[f-i+1][k];//d[i]是在逆序存储 } for(int i=f;i>=2;i--){ cout<<d[i]<<" ";//为了不输出多余的空格 } cout<<d[1]<<endl; return 0; }
十五、滑雪
记忆化搜索,加上动态规划,注意搜索过程的写法
int n,c,map[101][101]; int f[101][101];//存储结果的 int dis[4][2]={{0,1},{0,-1},{1,0},{-1,0}}; int seach(int x,int y){ if(f[x][y]) return f[x][y]; //记忆化搜索 int t=1,temp; for(int i=0;i<4;i++){ int xx=x+dis[i][0]; int yy=y+dis[i][1]; if(xx>=1&&xx<=n&&yy>=1&&yy<=c&&map[xx][yy]>map[x][y]){ temp=seach(xx,yy)+1; if(temp>t) t=temp;//这里是存储最优 } } f[x][y]=t; //如果没有上面的的过程,就会直接返回1,然后在不断的搜索过程中会变大,也就是返回的t return t; } int main(){ cin>>n>>c; for(int i=1;i<=n;i++) for(int j=1;j<=c;j++){ cin>>map[i][j]; } int t,ans=-9999; for(int i=1;i<=n;i++) for(int j=1;j<=c;j++){ t=seach(i,j); f[i][j]=t;//记得保存结果 if(t>ans) ans=t; } cout<<ans<<endl; return 0; }
十六、糖果
由于在维护世界和平的事务中做出巨大贡献,Dzx被赠予糖果公司2010年5月23日当天无限量糖果免费优惠券。在这一天,Dzx可以从糖果公司的N件产品中任意选择若干件带回家享用。糖果公司的N件产品每件都包含数量不同的糖果。Dzx希望他选择的产品包含的糖果总数是K的整数倍,这样他才能平均地将糖果分给帮助他维护世界和平的伙伴们。当然,在满足这一条件的基础上,糖果总数越多越好。Dzx最多能带走多少糖果呢?
注意:Dzx只能将糖果公司的产品整件带走。
【输入】
第一行包含两个整数N(1≤N≤100)和K(1≤K≤100)。
以下N行每行1个整数,表示糖果公司该件产品中包含的糖果数目,不超过1000000。
【输出】
符合要求的最多能达到的糖果总数,如果不能达到K的倍数这一要求,输出0。
int n,k; int a[101]; int f[101][101];//题目说了k的大小,就要考虑把k当成状态 //f[i][j]的意思是前i个物品里面余数为j的最多糖果数 int main(){ cin>>n>>k; for(int i=1;i<=n;i++) cin>>a[i]; memset(f,-128,sizeof(f));//赋值为负无穷 f[0][0]=0; f[1][0]=0; f[1][a[1]%k]=a[1];//取第一件 for(int i=2;i<=n;i++){ for(int j=0;j<k;j++){//把余数当作状态 f[i][j]=max(f[i-1][j],f[i-1][(k+j-a[i]%k)%k]+a[i]);//取a[i]和不取a[i] //注意这里的写法(j-a[i]%k+k)%k 因为有可能是负数嘛 } } if(f[n][0]!=0) cout<<f[n][0]<<endl; else cout<<"0"<<endl; return 0; }
十七、鸡蛋的硬度
最近XX公司举办了一个奇怪的比赛:鸡蛋硬度之王争霸赛。参赛者是来自世界各地的母鸡,比赛的内容是看谁下的蛋最硬,更奇怪的是XX公司并不使用什么精密仪器来测量蛋的硬度,他们采用了一种最老土的办法--从高度扔鸡蛋--来测试鸡蛋的硬度,如果一次母鸡下的蛋从高楼的第a层摔下来没摔破,但是从a+1层摔下来时摔破了,那么就说这只母鸡的鸡蛋的硬度是a。你当然可以找出各种理由说明这种方法不科学,比如同一只母鸡下的蛋硬度可能不一样等等,但是这不影响XX公司的争霸赛,因为他们只是为了吸引大家的眼球,一个个鸡蛋从100 层的高楼上掉下来的时候,这情景还是能吸引很多人驻足观看的,当然,XX公司也绝不会忘记在高楼上挂一条幅,写上“XX公司”的字样--这比赛不过是XX 公司的一个另类广告而已。
勤于思考的小A总是能从一件事情中发现一个数学问题,这件事也不例外。“假如有很多同样硬度的鸡蛋,那么我可以用二分的办法用最少的次数测出鸡蛋的硬度”,小A对自己的这个结论感到很满意,不过很快麻烦来了,“但是,假如我的鸡蛋不够用呢,比如我只有1个鸡蛋,那么我就不得不从第1层楼开始一层一层的扔,最坏情况下我要扔100次。如果有2个鸡蛋,那么就从2层楼开始的地方扔……等等,不对,好像应该从1/3的地方开始扔才对,嗯,好像也不一定啊……3个鸡蛋怎么办,4个,5个,更多呢……”,和往常一样,小A又陷入了一个思维僵局,与其说他是勤于思考,不如说他是喜欢自找麻烦。
好吧,既然麻烦来了,就得有人去解决,小A的麻烦就靠你来解决了:)
【输入】
输入包括多组数据,每组数据一行,包含两个正整数n和m(1≤n≤100,1≤m≤10),其中n表示楼的高度,m表示你现在拥有的鸡蛋个数,这些鸡蛋硬度相同(即它们从同样高的地方掉下来要么都摔碎要么都不碎),并且小于等于n。你可以假定硬度为x的鸡蛋从高度小于等于x的地方摔无论如何都不会碎(没摔碎的鸡蛋可以继续使用),而只要从比x高的地方扔必然会碎。
对每组输入数据,你可以假定鸡蛋的硬度在0至n之间,即在n+1层扔鸡蛋一定会碎。
【输出】
对于每一组输入,输出一个整数,表示使用最优策略在最坏情况下所需要的扔鸡蛋次数。
这道题当时做的时候完全无思路,后来看了题解(也是有点绕)之后觉得,这就是个逻辑题and数学题
int h,n; /* 这个题目有很多地方都有分析,我也看了好几个解答,但都说得不太明白(也许是我笨),我结合听的讲座和各家分析,加上自己的试验作以下分析。 这时的策略大体如下:第一,最简单的和种情况就是只有一只鸡蛋,那只能从一楼开始一层一层往上走,如果第一楼就破了,那硬度就是0,否则总能找到破 与不破的两层。那意味着硬度为多少就得比硬度数多扔一次就好。如果题目给的楼层是100,那最坏的可能就是硬度为99,必须扔100次。(硬度不能为100, 题目说了硬度小于n,也意味着从顶楼扔下,鸡蛋一定会碎,但这一次必须要扔)。第二,如果有两枚鸡蛋呢?那第一次就不用在第一楼扔,那在多少楼扔合 适?答案是第14楼。那要是破了呢?说明硬度最大13,同时我们手里的鸡蛋也只有一枚了,那办法就只有一个了:从第一楼开始往上,根据刚才的经验,最 多再扔13次就好(1-13楼全试,只有一枚鸡蛋,不能冒险哟)。那要是没破呢?又在哪扔呢?第27楼。要是破了,硬度在14-26之间,最多再试12次就可以了, 加上之前14楼和27楼两次,共14次。要是没破,下一次选在39楼。同样要是破了,就从28开始往上走,最多走到第38楼,共14次,要是没破,就选50楼。。。。 ,以后每多一次间距就少一,均能保证14次能测出硬度。第三,为什么是14楼开始?10楼不行么?如果选10楼,要是破了,当然比刚才更快,那要是没破呢? 下一次选多少楼?20?然后30、40、50、60、70、80、90?到了60己经扔了6次了,要是破了,最坏的情况是要再扔9次,在是没破又如何继续?明显不是最佳 策略。那为什么是14次?按照刚才的策略1+2+3+...+14=105超过100了,说明14次一定能测出结果。第四,要是鸡蛋有更多个呢?比如有三个,第一次可以考虑 对分:第50楼,要是不破,再二分,75楼.....,要是破了,就相当于两个鸡蛋测50楼,1+2+3+...+10=55,再有10次就可以搞定了。如果有四个鸡蛋呢?两次 就把范围缩小到25楼了,1+2+3+...+7=28,共需9次。那要是鸡蛋无限呢,最多也是全二分,2^7=128,这个就是最效率的了,换句话说,对100层楼而言,多于 7个鸡蛋也没用。第五,如果有m个鸡蛋n层楼呢?我们用f[n][m]记最少的次数。那可以枚举从第k层楼扔下,有两种情况:(1)鸡蛋破了,那硬度在1-k-1之间 ,我们得用余下的m-1个鸡蛋测试k-1层楼,这时f[n][m]=f[k-1][m-1],(2)鸡蛋没破。那再测余下的k~n层楼就好,这时f[n][m]=f[n-k][m],题目要求是最坏 的情况,故需要取这两种情况的最大值。故f[n][m]=max(f[k-1][m-1],f[n-k][m])+1。对所有的k的取值得到若干个f[n][m],这个我们可以选择k值,故把这些 值再取最小值。f[n][m]=min(f[n][m],max(f[k-1][m-1],f[n-k][m])+1),这就是帅气的状态转移方程了 */ int f[101][11]; int main(){ while(cin>>h>>n){ memset(f,0,sizeof(f)); for(int i=1;i<=h;i++){ for(int j=1;j<=n;j++) f[i][j]=i; //记住这个初值 } for(int i=1;i<=h;i++){ for(int j=2;j<=n;j++){ for(int k=1;k<=i;k++){ f[i][j]=min(f[i][j],max(f[k-1][j-1],f[i-k][j])+1); //一个是破了,剩余需要检验的是k-1层, //另一个是没破,剩余需要检验i-k层 //最坏情况:所以用max } } } cout<<f[h][n]<<endl; } return 0; }
十八、大盗A福
这道题想通了就是很简单的嘛,就是在现在的情况下,要么九偷上上个要么就偷上上上个
int t,n; int a[100001]; int f[100001]; //偷前i个能够得到最大值 int main(){ cin>>t; while(t--){ cin>>n; memset(a,0,sizeof(a)); memset(f,0,sizeof(f)); for(int i=1;i<=n;i++) cin>>a[i]; f[0]=0; f[1]=a[1]; f[2]=a[2]; int maxx=a[1]; if(n>1&&a[2]>a[1]) maxx=a[2]; //注意条件n>1 //f[i]表示前i个店铺最多能得到的 for(int i=3;i<=n;i++){ f[i]=max(f[i-3],f[i-2])+a[i]; //状态转移方程:要么偷上上个,要么偷上上上个 if(maxx<f[i]) maxx=f[i]; } cout<<maxx<<endl; } return 0; }
十九、股票买卖
最近越来越多的人都投身股市,阿福也有点心动了。谨记着“股市有风险,入市需谨慎”,阿福决定先来研究一下简化版的股票买卖问题。
假设阿福已经准确预测出了某只股票在未来N天的价格,他希望买卖两次,使得获得的利润最高。为了计算简单起见,利润的计算方式为卖出的价格减去买入的价格。
同一天可以进行多次买卖。但是在第一次买入之后,必须要先卖出,然后才可以第二次买入。
现在,阿福想知道他最多可以获得多少利润。
这道题不难(优点序列的意思),两边循环查找,分别用两个数组来表示就可以了,最后maxx=max(maxx,q[i]+f[i]);
int sum=0,n,t; int a[100001]; int f[100001];//顺序的 int q[100001];//逆序的 int main(){ cin>>t; while(t--){ scanf("%d",&n); int minn=999999,maxx=-999999; memset(a,0,sizeof(a)); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } //这里用cin超时了!!!数据大时还是用scanf //我过于注重顺序了 //导致思维局限‘ f[0]=0; q[n+1]=0;//初始化 for(int i=1;i<=n;i++){ minn=min(a[i],minn); f[i]=max(f[i-1],a[i]-minn);//要么这天不卖,要么这天卖 } for(int i=n;i>=1;i--){ maxx=max(maxx,a[i]); q[i]=max(q[i+1],maxx-a[i]);//要么这天不买,要么这天买入 } maxx=-9999; for(int i=1;i<=n;i++){ maxx=max(maxx,q[i]+f[i]); } printf("%d ",maxx); } return 0; }
二十、鸣人的影分身
在火影忍者的世界里,令敌人捉摸不透是非常关键的。我们的主角漩涡鸣人所拥有的一个招数——多重影分身之术——就是一个很好的例子。
影分身是由鸣人身体的查克拉能量制造的,使用的查克拉越多,制造出的影分身越强。
针对不同的作战情况,鸣人可以选择制造出各种强度的影分身,有的用来佯攻,有的用来发起致命一击。
那么问题来了,假设鸣人的查克拉能量为M,他影分身的个数最多为N,那么制造影分身时有多少种(用K表示)不同的分配方法?(影分身可以被分配到0点查克拉能量)
int t,n,m; int f[11][11]; int main(){ cin>>t; for(int i=0;i<=10;i++){//能量 for(int j=0;j<=10;j++){//身体数 if(j==1||i==0||i==1) f[i][j]=1; //一个身体、0点或1点能量-----都只有一种方案 else if(j>i) f[i][j]=f[i][i]; else f[i][j]=f[i-j][j]+f[i][j-1]; //第一种情况:每个分身都有1点能量加上多余的能产生的 //第二种情况:减少一个身体能够产生的 //分为无0和有0 !!!!!记住 } } while(t--){ cin>>n>>m; cout<<f[n][m]<<endl; } return 0; }
二十一、数的划分
将整数n分成k份,且每份不能为空,任意两份不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种分法被认为是相同的。
1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。 输出一个整数,即不同的分法。
int f[8][201]; int main() { int m,n; cin>>m>>n; //qiushu heshu f[0][0]=1; for(int i=1;i<=n;i++){ for(int j=i;j<=m;j++){ f[i][j]=f[i-1][j-1]+f[i][j-i]; //要么划分给第i个数1,要么用剩下的j-i去分 } } cout<<f[n][m]<<endl; return 0; }