一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情。
石子合并
描述
在一个圆形操场的四周摆放着n堆石子(n<= 100),现要将石子有次序地合并成一堆。规定每次只能选取相邻的两堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。编一程序,读入石子堆数n及每堆的石子数(<=20)。选择一种合并石子的方案,使得做n-1次合并,得分的总和最小; 比如有4堆石子:4 4 5 9 则最佳合并方案如下: 4 4 5 9 score: 0 8 5 9 score: 8 13 9 score: 8 + 13 = 21 22 score: 8 + 13 + 22 = 43
输入
可能有多组测试数据。 当输入n=0时结束! 第一行为石子堆数n(1<=n<=100); 第二行为n堆的石子每堆的石子数,每两个数之间用一个空格分隔。
输出
合并的最小得分,每个结果一行。
输入样例
4 4 4 5 9 0
输出样例
43
思路
一开始以为这个题和合并果子一样是个贪心的题,拿着优先队列就开始一波贪心,WA了。才发现是合并相邻的石子。那这个题就和上一个计算矩阵连乘积的题几乎一样了。但是也有不一样的。这个题的序列是循环序列,并且为了记录每一次合并对答案的贡献我们还需要记录前缀和(这里求前缀和是为了优化时间复杂度,不求前缀和的话每次求贡献需要O(n)),由于这是循环数列,求前缀和时候需要求前2 * n的。数列大小是2 * n(a[1],a[2],...,a[n],a[1],a[2],...a[n]).设sum[i]为数组a[i]前i个的和,那么要求l-r之间a[]之和,就可以O(1) 求得,即sum[r]-sum[l-1]。设dp[i][j]为合并i-j之间所有石子的最小代价,初始化为0x3f3f3f3f(即无穷大),dp[i][i]=0(i from 1 to n即所有的单堆石子合并代价都是0)。枚举长度len(len from 2 to n),嵌套枚举左边界l(l from 1 to n),那么有边界r=l+len-1,由此可以更新dp[l][r],为了求dp[l][r],还需要考虑是哪两段石子进行了合并,所以需要枚举l-r之间的mid(l<=mid<=r-1),这样合并石子区段l-mid和mid+1 - r 。dp[l][r]=min{dp[l][r],dp[l][mid]+dp[mid+1][r]+sum[r]-sum[l-1]};(每一次合并除了那两个区段合并的代价,还需要注意合并当前这两个区段的代价是区段石子的总和:sum[r]-sum[l-1]).
最后需要枚举一遍答案求得最优解:ans=min(ans,dp[i][i+len-1]);(i from 1 to n 需要找到合并的起点)。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=200+50;
int a[maxn],n,dp[maxn][maxn],sum[maxn];
int min(int x,int y){
if(x<=y)return x;
return y;
}
int main(){
while(1){
int i;
cin>>n;
if(n==0)break;
memset(dp,0x3f3f3f3f,sizeof(dp));
for(i=1;i<=n;i++)cin>>a[i],sum[i]=sum[i-1]+a[i],dp[i][i]=dp[i+n][i+n]=0;
for(i=1;i<=n;i++)sum[i+n]=sum[i+n-1]+a[i];
for(int len=2;len<=n;len++){
for(int l=1;l<=2*n-len+1;l++){
int r=l+len-1;
for(int m=l;m<=r-1;m++){
dp[l][r]=min(dp[l][r],dp[l][m]+dp[m+1][r]+sum[r]-sum[l-1]);
}
}
}//len从2到n 更新
int ans=0x3f3f3f3f;
for(i=1;i<=n;i++){
ans=min(ans,dp[i][i+n-1]);
}
cout<<ans<<endl;
}
return 0;
}
旅游预算
描述
一个旅行社需要估算乘汽车从某城市到另一城市的最小费用,沿路有若干加油站,每个加油站收费不一定相同。旅游预算有如下规则: 若油箱的油过半,不停车加油,除非油箱中的油不可支持到下一站;每次加油时都加满;在一个加油站加油时,司机要花费2元买东西吃;司机不必为其他意外情况而准备额外的油;汽车开出时在起点加满油箱;计算精确到分(1元=100分)。编写程序估计实际行驶在某路线所需的最小费用。
输入
第一行为起点到终点的距离(实数) 第二行为三个实数,后跟一个整数,每两个数据间用一个空格隔开。其中第一个数为汽车油箱的容量(升),第二个数是每升汽油行驶的公里数,第三个数是在起点加满油箱的费用(精确到分),第四个数是加油站的数量。(〈=50)。接下去的每行包括两个实数,每个数据之间用一个空格分隔,其中第一个数是该加油站离起点的距离,第二个数是该加油站每升汽油的价格(元/升)。加油站按它们与起点的距离升序排列。所有的输入都有一定有解。
输出
共两行,每行都有换行 第一行为一个实数和一个整数,实数为旅行的最小费用,以元为单位,精确到分,整数表示途中加油的站的N。第二行是N个整数,表示N个加油的站的编号,按升序排列。数据间用一个空格分隔,最后一个数据后也输出空格,此外没有多余的空格。
输入样例
516.3 15.7 22.1 20.87 3 125.4 1.259 297.9 1.129 345.2 0.999
输出样例
38.09 1
2
思路
由题意可知:如果在第i个加油站要加油,必须保证到达第i+1个加油站的时候油箱的油少于一半。并且一旦加油,油箱的油就加满了。dp[i]表示到达第i个加油站的最小耗费,初始化为1e9(无穷大)。dp[0]: 起点加满油箱的费用 。a[i].d表示第i个加油站距离起点的距离,a[i].w表示第i个加油站加油的单价费用。枚举当前到达的加油站i(i from 1 to n),然后再枚举上一次加油的加油站j(j from 0 to i-1,0就是起点)。if(a[i].d-a[j].d>road||(a[i].d-a[j].d) * 2<road)如果无法从j到达i或者从j到达i油箱油量过半都无法考虑,其余的情况dp[i]=min(dp[i],dp[j]+ned * a[i].w+2),因为最后答案要输出在哪些加油站加了油,所以需要记录上一次加油的加油站pre[i]=j;最后再看从哪个加油站i到终点代价最小(i from 0 to n),必须保证油量可以从i到终点,ans为最终答案,ans=min(ans,dp[i]),并且记录最后一个加油站last。由last为终点前的加油站,进行回溯查询加过油的加油站。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=1000+50;
struct Node{
double w,d;
}a[maxn];
double s,v,p,dp[maxn];int n,pre[maxn],t[maxn];
double min(double x,double y){
if(x<=y)return x;
return y;
}
int main(){
int i,j;
cin>>s>>v>>p>>dp[0]>>n;
for(i=1;i<=n;i++)cin>>a[i].d>>a[i].w,dp[i]=1e9;
double road=v*p;
for(i=1;i<=n;i++){
for(j=0;j<=i-1;j++){
if(a[i].d-a[j].d>road||(a[i].d-a[j].d)*2<road)continue;//注意油量超过一半并且可以到达下一个点不加油!
double ned=(a[i].d-a[j].d)/p;
if(dp[i]>dp[j]+ned*a[i].w+2){
dp[i]=dp[j]+ned*a[i].w+2;
pre[i]=j;
}
}
}
double ans=0x3f3f3f3f;int last=0,tot=0;
for(i=0;i<=n;i++){
if(road<s-a[i].d)continue;
if(ans>dp[i]){
ans=dp[i];
last=i;
}
}
while(last!=0){
t[++tot]=last;
last=pre[last];
}
printf("%.2lf %d\n",ans,tot);
for(i=tot;i>=1;i--)cout<<t[i]<<' ';cout<<endl;
return 0;
}