现在的位置: 首页 > 综合 > 正文

POJ 3373 Changing Digits (记忆化搜索)

2017年04月27日 ⁄ 综合 ⁄ 共 3075字 ⁄ 字号 评论关闭

                     Problem
F - Changing Digits

   [Description]

   给出2个整数n(n<10^100)和k(k<10000),求满足以下条件的整数m
   1、m与n位数相同
   2、m能被k整除
   3、满足以上两点时,m和n在相同位置的地方,数字不同的个数最少
   4、满足以上三点时,m值最小
   

   [Intput]
   输入包含多组测试数据,每组数据占一行两个数,n,k (n > k),均不含前导0。
   

   [Output]
   对于每组数据输出一个适合的m
   

   [Sample Input]
   2
   2
   619103
   3219
   

   [Sample Onput]
   2
   119103




    这道题是考试的时候遇到的,最后一道,F题,分析了一下,没什么思路,就用记忆化搜索试了试,结果越写越有感觉,就觉得记忆化搜索是正解,然后。。。就A了。

    大致过程是这样的:n的第i位(从0开始计数)取j时模上k的值我们都可以预处理出来存在f[i][j]中,如:23333,其中第2位为3,若k=9,则f[2][3]=300%9=3。那么我们就可以得到n%k的值,记为reet。设n的第i位为num[i],则将num[i]改为j后新的数模上k的值ret为:

    ret=(n-f[i][num[i]]+f[i][j])%k=(reet-f[i][num[i]]+f[i][j]+k)%k

   加k是因为可能为负值。则将n的某一位改变之后新的数模上k的余数我们都可以计算出来,基于此,我们就可以设计状态了:

     设n的长度为l。

     用bool dfs(int u,int remain)记录能否通过修改u到l这些位置上的数使得新得到的数模上k等于0。如果能够,返回真,同时记录下最少修改的n的位数与在这种情况下第u位的值。

     由于题目要求在修改n的位数最少时,使得m尽量小。所以我们在枚举u这一位放什么数字的时候从小到大枚举,这样我们就只需根据长度来更新就可以了,因为按照这样的顺序得到的第一个长度为len的情况m一定是最小的。最后再输出就行了。

     我的代码虽然长了点,但写的结构还是蛮清楚的,也有很多注释,代码如下:

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;

/*
  (a-b)%k=((a%k-b%k)+k)%k
*/

int k,l,reet;

char ss[100+10];

int num[100+10],f[100+10][10+10],vis[100+10][10000+10],dp[100+10][10000+10],len[100+10][10000+10];

//num[i]:n的第i位上的数(n是从高位到低位存的)
//f[u][i]:n的第u位取i时的余数
//vis[u][remain]:标记当前状态是否到达过。
//dp[u][remain]: 当前状态的最优解时第u位的取值。
//len[u][remain]:当前状态的最优长度 

bool dfs(int u,int remain)//在剩下的u位中,余数为remain,能否达到。 
{
   if(u==l)return false;//搜到第l位仍然没有将余数消为0,返回不能到达 
   if(vis[u][remain]!=-1)return vis[u][remain];//标记都懂得 
  //先考虑达到remain的最小改变位数,再考虑最小 
   
   int ret;
   //初始化 
   len[u][remain]=l+2;
   dp[u][remain]=num[u];
   
   int ok=0,pi;
   
   //搜索顺序由小到大,保证一旦更新答案,即为当前长度的最优解 
   
   
   for(int i=0;i<=9;i++)
   {
   	 if(u==0&&i==0)continue;//第一位不能取0 
	 if(i!=num[u])pi=1;//标记是否改变当前的数 
	 else pi=0;
   	 ret=remain-f[u][num[u]]+f[u][i];//计算 
   	 while(ret<0)ret+=k;//计算改变后余数
   	 ret%=k;
   	 if(ret==0)//目标,更新 
   	 {
   	       if(1<len[u][remain])//更新只看长度 
   	       {
   	       	  len[u][remain]=1;
   	       	  dp[u][remain]=i;
   	       	  ok=1;//标记有解 
   	       }
   	 }
   	 else if(dfs(u+1,ret))//能到达目标,更新 
   	 {
   	    if(len[u+1][ret]+pi<len[u][remain])//更新只看长度
		{
		  len[u][remain]=len[u+1][ret]+pi;  
		  dp[u][remain]=i;	
		  ok=1;//标记有解 
		}	
   	 }
   }
   
   if(ok)//有解,返回真 
   {
   	 //printf("len[%d][%d]=%d dp[%d][%d]=%d\n",u,remain,len[u][remain],u,remain,dp[u][remain]);
     return vis[u][remain]=true;
   }
   return vis[u][remain]=false;//返回假 
  
}

void init()
{
  memset(vis,-1,sizeof(vis));
	
  l=strlen(ss);
  
  for(int i=0;i<l;i++)//处理每一位的数字 
  {
  	num[i]=ss[i]-'0';
  }
  
  for(int i=l-1;i>=0;i--)//递推第i位为j时%k的值 
  {
  	f[i][0]=0;
  	if(i<l-1)f[i][1]=(f[i+1][5]+f[i+1][5])%k;//不是个位,用小一位的数更新 
  	else f[i][1]=1%k;//是个位,用1更新 
  	for(int j=2;j<=9;j++)
  	f[i][j]=(f[i][j-1]+f[i][1])%k;
  }
  
  reet=0;//计算余数 
  
  for(int i=0;i<l;i++)
  {
  	reet=(reet+f[i][num[i]])%k;
  }
  
}

void print(int u,int remain)
{
   if(remain==0)//已经到达状态,接下的原样输出 
   {for(int i=u;i<l;i++)printf("%d",num[i]);printf("\n");return;}
   else printf("%d",dp[u][remain]);
   if(u==l-1)//边界,返回 
   {
   	printf("\n");
   	return;
   }
   int ret=remain-f[u][num[u]]+f[u][dp[u][remain]];
   while(ret<0)ret+=k;
   ret%=k;//计算余数,继续输出 
   print(u+1,ret);
}

int main()
{ 

  while(scanf("%s%d",ss,&k)==2)
  {
      init();//预处理每一位为(0-9)这些数时的余数 
	
	if(reet==0)//n%k==0,最优解就是n 
	{
		printf("%s\n",ss);
		continue;
	}
	if(l==1)//长度为1,n%k!=0,0为最优解 
	{
		printf("0\n");
		continue;
	}
	
	dfs(0,reet);//搜索 
	
	print(0,reet);//输出 
		
  }
	
  return 0;
}

      考完以后也去搜了下题解,有直接DP的,但本人奇弱,无法确定DP的顺序,只好写了记忆化搜索,个人感觉简便许多,代码的可读性更强,debug也要容易一点。

   还有大牛写深搜+强剪枝,居然也A了,看到那些神一般的剪枝与定理,只有膜拜之。


   这道题就这样了,只要想到了还是蛮好写的。

抱歉!评论已关闭.