题意:有n个数的数组a 每一个数属于[0,30]
现在要找一个n个数的数组b 使得|ai-bi|的和最小 对于任意i,j,i!=j,bi,bj最大公因数为1
(1 ≤ n ≤ 100)
策略: 首先这样的b数组肯定存在 我们可以用n个1来实现,而且ai的范围在[0,30]所以 我们在某一个位置上用1和用59的效果是一样的,也就是说b的每一位上我们的选择只要在[1,58]之间就可以了。然后盲目搜索复杂度非常高。所以我们可以通过之前的状态来选择下一位上的数。
假设我们已经在前i位选择了i个数 已经使用了质数prime[k1,k2...kn]的最小和为 sum,那么我们在选择下一位i+1的时候,我们从1-58中找到一个质数没有在之前使用过,即不在prime[k1,k2,..kn]中的数,并把这个数使用的质数加入到prime状态中即可实现转移。
设dp[i][S]为前i位,素数使用状况为S的最小和,那么下一个数bi+1选择后,使用素数状况为S1,且S1&S=0,我们可以更新dp[i+1][S|S1]=min(dp[i+1][S|S1],dp[i][S]+|ai-bi|);
因为最后要输出数组,要用rec记录一下每个状态选择的数。找到第i位状态为S的最小dp值,最后通过S状态反向输出即可。
素数大约有16个,所以S我们可以二进制表示,16个二进制位每一位01表示对应的素数是否用过,状态压缩dp
复杂度在 2^16*n*58
//Author:Wenjun Shi
//on 2016/7/14
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <climits>
#define MAX_N 105
#define P_NUM 16
#define MAX_NUM 58
using namespace std;
int n,m;
int prime[P_NUM]= {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};//需要的素数
int use[60]; //数i包含的素数的状态位情况 二进制表示
int rec[MAX_N][1<<P_NUM];//到第i位,用了哪些素数产生最小值时第i位所选的数
int dp[MAX_N][1<<P_NUM];//到第i位,用了哪些素数产生的最小值
int a[MAX_N];
int ans[MAX_N];
int main()
{
cin>>n;
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
for(int i=1; i<=MAX_NUM; i++)
{
for(int j=0; j<P_NUM; j++)
{
if(i%prime[j]==0) use[i]|=1<<j;
}
}
memset(rec,-1,sizeof(rec));
memset(dp,0x7f,sizeof(dp));
const int INF=dp[0][0];
dp[0][0]=0;
for(int i=1; i<=n; i++)
{
for(int j=0; j<1<<P_NUM; j++)
{
for(int k=1; k<=MAX_NUM; k++)
{
if(dp[i-1][j]!=INF&&(use[k]&j)==0)
{
int d=abs(a[i]-k);
if(dp[i][j|use[k]]>dp[i-1][j]+d)
{
dp[i][j|use[k]]=dp[i-1][j]+d;
rec[i][j|use[k]]=k;
}
}
}
}
}
int Min=INF,s;
for(int j=0;j<1<<P_NUM;j++)
if(dp[n][j]<Min)
{
Min=dp[n][j];
s=j;
}
for(int i=n;i>0;i--)
{
ans[i]=rec[i][s];
s^=use[ans[i]];
}
for(int i=1;i<=n;i++)
cout<<ans[i]<<' ';
return 0;
}