【学海拾遗】用区间dp巧解北航往年考题

2,058 阅读4分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

题意(大概)

(这是一道北航算法课期末题)

已知最大公约数的计算满足结合律,即gcd(a,b,c)=gcd(a,gcd(b,c))=gcd(gcd(a,b),c)gcd(a,b,c)=gcd(a,gcd(b,c))=gcd(gcd(a,b),c).

另外计算gcd时有公式 gcd(a,b)=gcd(b,a%b)gcd(a,b)=gcd(b,a\%b) ,当第二个参数为0时停止递归.计算不同的两个数字的公约数时就有不同的递归次数.

因此,给定nn和一连串的数字a1,a2,...,ana_1,a_2,...,a_n,使用不同的计算顺序计算gcd(a1,...,an)gcd(a_1,...,a_n)具有不同的递归次数.我们要做的是找到总递归次数最小的计算顺序.

要求第一行输出gcd(a1,...,an)gcd(a_1,...,a_n)的运算结果,第二行输出对应的结合顺序(优先向左结合,格式按照样例),第三行输出递归次数的最小值.具体可参考样例理解.

样例:

输入:

4
1 1 1 1 

输出:

1
(((12)3)4)
6

样例解释:

gcd(1,1,1,1)=1gcd(1,1,1,1)=1,故第一行输出1.

由于全是1,所以什么顺序计算所需的递归次数都是一样的,但是需要向左优先结合,故如第二行输出,用下标代表对应的数字.

计算一次gcd(1,1)gcd(1,1)需要2次递归,计算gcd(1,1,1,1)gcd(1,1,1,1)要算3次gcd(1,1)gcd(1,1),故递归次数一共为6.

思路

基本思路和矩阵乘法链基本一致(可以参考算法导论的15.2节),动态规划.

先有如下假定:

gcdn(x,y):计算gcd(x,y)的递归次数,xy是整数gcdn(x,y):计算gcd(x,y)的递归次数,x和y是整数

dp1(i,j):gcd(ai,...,aj)的最小递归次数dp1(i,j):gcd(a_i,...,a_j)的最小递归次数

dp2(i,j):gcd(ai,...,aj)的计算结果dp2(i,j):gcd(a_i,...,a_j)的计算结果

dp3(i,j):dp3(i,j)=kdp3(i,j):dp3(i,j)=k 即代表 kk 是最小计算次数下该次结合的分界点,即若 gcd(ai,...,aj)gcd(a_i,...,a_j) 通过 gcd(gcd(ai,...,ak),gcd(ak+1,...,aj))gcd(gcd(a_i,...,a_k),gcd(a_{k+1},...,a_j)) 的方式计算可以获得最小递归次数,那么 dp3(i,j)=kdp3(i,j)=k

显然 dp(i,i)dp(i,i) 为0 , dp(i,i+1)dp(i,i+1) 可以直接计算得出.对于任意 j>i+1j>i+1,我们要做的就是遍历 iijj 间的所有分界点 kk ,我们假设先通过最优方式计算了 gcd(ai,...,ak)gcd(a_i,...,a_k)gcd(ak+1,...,aj)gcd(a_{k+1},...,a_j) ,最后就剩这两个计算完毕后剩下的两个数(对应的dp2),最后计算这两个数的gcd.把这个过程的递归次数加起来,对于每个k都计算一遍然后取最小值.由此得到,

dp1(i,j)=minikj1(dp1(i,k)+dp1(k+1,j)+gcdn(dp2(i,k),dp2(k+1,j)))dp1(i,j)=min_{i\leq k\leq j-1}\bigg(dp1(i,k)+dp1(k+1,j)+gcdn(dp2(i,k),dp2(k+1,j))\bigg)

dp2(i,j)dp2(i,j) 的求法则很简单,对于任意ikj1i\leq k\leq j-1,

dp2(i,j)=gcd(dp2(i,k),dp2(k+1,j))dp2(i,j)=gcd(dp2(i,k),dp2(k+1,j))

而关于 dp3(i,j)dp3(i,j) 的求法,就是在计算 dp1dp1 遍历 kk 寻找最小值的时候,记录取到最小值时的 kk 就行.由于题目要求优先向左结合,所以当有多个 kk 最小时,取最大的 kk.

目前就剩1个小问题没有解决,那就是怎么通过dp3来输出形如 (((12)3)4) 的输出结果,那就是递归,同学可以在代码部分理解.

代码

由于不仅需要计算 gcd(x,y)gcd(x,y) 的值,还需要计算递归次数,因此我给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部分是三层循环,复杂度为 O(n3)O(n^3) ;每个 pt 函数的开始和结尾都必定会输出两个括号,而答案固定有 2(n1)2*(n-1) 个括号,固pt函数的复杂度为 O(n)O(n) .因此总共的复杂度为 O(n3)O(n^3) ,由于常数较小,因此可以险过此题.