Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
题意(大概)
(这是一道北航算法课期末题)
已知最大公约数的计算满足结合律,即.
另外计算gcd时有公式 ,当第二个参数为0时停止递归.计算不同的两个数字的公约数时就有不同的递归次数.
因此,给定和一连串的数字,使用不同的计算顺序计算具有不同的递归次数.我们要做的是找到总递归次数最小的计算顺序.
要求第一行输出的运算结果,第二行输出对应的结合顺序(优先向左结合,格式按照样例),第三行输出递归次数的最小值.具体可参考样例理解.
样例:
输入:
4
1 1 1 1
输出:
1
(((12)3)4)
6
样例解释:
,故第一行输出1.
由于全是1,所以什么顺序计算所需的递归次数都是一样的,但是需要向左优先结合,故如第二行输出,用下标代表对应的数字.
计算一次需要2次递归,计算要算3次,故递归次数一共为6.
思路
基本思路和矩阵乘法链基本一致(可以参考算法导论的15.2节),动态规划.
先有如下假定:
即代表 是最小计算次数下该次结合的分界点,即若 通过 的方式计算可以获得最小递归次数,那么
显然 为0 , 可以直接计算得出.对于任意 ,我们要做的就是遍历 到 间的所有分界点 ,我们假设先通过最优方式计算了 和 ,最后就剩这两个计算完毕后剩下的两个数(对应的dp2),最后计算这两个数的gcd.把这个过程的递归次数加起来,对于每个k都计算一遍然后取最小值.由此得到,
的求法则很简单,对于任意,
而关于 的求法,就是在计算 遍历 寻找最小值的时候,记录取到最小值时的 就行.由于题目要求优先向左结合,所以当有多个 最小时,取最大的 .
目前就剩1个小问题没有解决,那就是怎么通过dp3来输出形如 (((12)3)4) 的输出结果,那就是递归,同学可以在代码部分理解.
代码
由于不仅需要计算 的值,还需要计算递归次数,因此我给gcd函数一个引用参数 x,可以返回计算次数.c语言的同学可以使用指针做到同样的功能.
ll gcd(ll a,ll b,int &x){
if(b==0)return a;
x=x+1;
return gcd(b,a%b,x);
}
以下是输出第二行的代码,通过dp3和递归
void pt(int l,int r){
if(l==r)printf("%d",l);
else if(l==r-1)printf("(%d%d)",l,r);
else{
printf("(");
pt(l,dp3[l][r]);
pt(dp3[l][r]+1,r);
printf(")");
}
}
总共的代码如下:
#include<iostream>
#include<cstdio>
#include<vector>
#include<cctype>
#include<cmath>
#include<string>
#include<cstring>
#include<queue>
#include<numeric>
#define fru(a,b,c) for(int a=b;a<=c;a++)
#define frd(a,b,c) for(int a=b;a>=c;a--)
#define fr(a,b) for(int a=0;a<b;a++)
#define pb push_back
#define sof sizeof
using namespace std;
using ll=unsigned long long;
ll rd(){
ll k=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){k=(k<<1)+(k<<3)+(c^48);c=getchar();}
return f>0?k:-k;
}
const int inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f;
const int maxn=1000+5;
ll gcd(ll a,ll b,int &x){
if(b==0)return a;
x=x+1;
return gcd(b,a%b,x);
}
ll dp1[maxn][maxn];//次数
ll dp2[maxn][maxn];//结果
ll dp3[maxn][maxn];//坐标
ll arr[maxn];
void pt(int l,int r){
if(l==r)printf("%d",l);
else if(l==r-1)printf("(%d%d)",l,r);
else{
printf("(");
pt(l,dp3[l][r]);
pt(dp3[l][r]+1,r);
printf(")");
}
}
int main(){
int n=rd();
fru(i,1,n)arr[i]=rd();
fru(i,1,n-1){
int x=0;
dp2[i][i+1]=gcd(arr[i],arr[i+1],x);
dp1[i][i+1]=x+1;
}
fru(i,1,n){
dp2[i][i]=arr[i];
}
fru(d,2,n-1){
fru(i,1,n-2){
int j=i+d;
int it=0;
ll mi1=linf;
ll mi2;
fru(k,i,j-1){
int x=0;//用来被gcd调用,储存递归次数
ll tmp=gcd(dp2[i][k],dp2[k+1][j],x);
if(dp1[i][k]+dp1[k+1][j]+x<=mi1){
mi1=dp1[i][k]+dp1[k+1][j]+x;
mi2=tmp;
it=k;
}
}
dp1[i][j]=mi1+1;
dp2[i][j]=mi2;
dp3[i][j]=it;
}
}
cout<<dp2[1][n]<<'\n';
pt(1,n);
cout<<'\n'<<dp1[1][n];
}
复杂度分析
计算dp部分是三层循环,复杂度为 ;每个 pt 函数的开始和结尾都必定会输出两个括号,而答案固定有 个括号,固pt函数的复杂度为 .因此总共的复杂度为 ,由于常数较小,因此可以险过此题.