给出长度为N的一个序列A,让我们找到一个等长度序列B,使得Σabs(Ai-Bi)最小,要求B数组中,任取两个数字的Gcd为1,如果存在多解,输出一个即可。
思路:
需要不断去想优化时间复杂度的一个题,蛮不错的。
①首先考虑,对于这样的一个数组B,其置为1的个数可以为无限个,因为任何数和1的gcd都一定是1.那么我们再根据Ai的数据范围(1~30)可以得知,我们对于Bi的取值范围,一定介于(1~59)之间,因为我们假设取了值60.那么是不如直接取1来的更优的(对于任何数来讲都是一定的)。
②我们既然界定了B数组的取值范围,那么接下来考虑这类问题的特性,既然提到了Gcd,那么我们也一定能够联想到素数。如果对于一个位子的取值为K的时候,我们知道,如果K%pi(pi表示某个素数)==0的话,我们之后的位子中,就不能再存在一个数字X,使得X%pi==0了。
所以我们对于当前特性,我们可以设定Dp【i】【j】表示我们dp过程进行到位子i,对于素数占用的情况为j的最优解。因为60以内的素数个数并不多(17个),所以我们接下来去状压dp即可。
③我们预处理出num【k】,表示我们数字k会占用的素数的二进制表示。那么对于状态转移方程不难写出(从当前状态转移到下一状态):
Dp【i+1】【j+num【k】】=min(Dp【i+1】【j+num【k】】,Dp【i】【j】+abs(k-a【i+1】));【需要保证(j&num[k])==0】;
过程维护一下即可,需要输出路径,我们再维护一个数组pre【i】【j】,表示转移到状态Dp【i】【j】的时候,最后拿取的数字。
然后逆序走一波就行了。
Ac代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<iostream>
#include<vector>
using namespace std;
int n;
int a[103];
int dp[103][(1<<17)];
int pre[103][(1<<17)];
int su[17]= {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59};
int num[65];
void init()
{
int end=(1<<17);
for(int i=0; i<=n; i++)
{
for(int j=0; j<end; j++)
{
dp[i][j]=0x3f3f3f3f;
}
}
memset(num,0,sizeof(num));
for(int i=2; i<=60; i++)
{
for(int j=0; j<17; j++)
{
if((i%su[j])==0)num[i]+=(1<<j);
}
}
}
int main()
{
while(~scanf("%d",&n))
{
int end=(1<<(17));
init();
for(int i=1; i<=n; i++)scanf("%d",&a[i]);
dp[0][0]=0;
for(int i=0; i<n; i++)
{
for(int j=0; j<end; j++)
{
if(dp[i][j]!=0x3f3f3f3f)
{
if(dp[i][j]+abs(a[i+1]-1)<dp[i+1][j])
{
dp[i+1][j]=dp[i][j]+abs(a[i+1]-1);
pre[i+1][j]=1;
}
for(int k=2; k<=60; k++)
{
if((num[k]&j)==0)
{
if(dp[i][j]+abs(k-a[i+1])<dp[i+1][j+num[k]])
{
dp[i+1][j+num[k]]=dp[i][j]+abs(k-a[i+1]);
pre[i+1][j+num[k]]=k;
}
}
}
}
}
}
int pos;
int output=0x3f3f3f3f;
for(int j=0; j<end; j++)
{
if(dp[n][j]<output)
{
pos=j;
output=dp[n][j];
}
}
int level=n;
vector<int>ans;
while(level>0)
{
ans.push_back(pre[level][pos]);
pos-=num[pre[level][pos]];
level--;
}
reverse(ans.begin(),ans.end());
for(int i=0;i<ans.size();i++)
{
printf("%d ",ans[i]);
}
printf("\n");
}
}