(有任何问题欢迎留言或私聊
题目链接及相关信息见于:Lrj的《算法竞赛入门经典》第二版P206
在古埃及,人们使用单位分数的和(即1/a,a是自然数)表示一切有理
数。 例如,2/3=1/2+1/6,但不允许2/3=1/3+1/3,因为在加数中不允许有相同的。
对于一个分数a/b,表示方法有很多种,其中加数少的比加数多的好,如果加数个数相
同,则最小的分数越大越好。 例如,19/45=1/5+1/6+1/18是最优方案。
输入:输入a,b,m。a,b意思同上,m表示有m个数不能做分母,接下来输入这m个数字。
套Lrj紫书上的模板,从小到大枚举深度上限maxd,dfs搜索求解
减枝:
1.每次枚举不是从1开始枚举,而是从最小的一个数x且满足1/x < a/b,从x开始枚举
2.得到上面的x后,定义乐观估价函数f^(n)=g^(n)+h^(n)。
g^(n)是当前深度,h^(n)是到目标需要的步数,这题的处理没有那么麻烦。
减枝就是h^(n)=maxd-g^(n)+1,如果h^(n)/x<=a/b时,break;
//2410ms
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long LL;
const int maxn = 100000+7;
int maxd;
LL a, b;
LL ans[maxn], v[maxn];
LL br[maxn],tot;
inline LL gcd(LL a,LL b){
return b==0?a:gcd(b,a%b);
}
LL get_first(LL a,LL b){//a/b<1/c->a*c
LL c=1;
while(a*creturn c;
}
bool better(int d){
for(int i=d;i>=0;--i){
if(v[i]!=ans[i]){//分解地更长或最大数字更小则return true
return ans[i]==-1||v[i]return false;
}
bool dfs(int d,LL from,LL aa,LL bb){
if(d==maxd){
for(int i=0;iif(br[i]==bb/aa)return false;
if(bb%aa)return false;//不能整除
v[d]=bb/aa;//当前深度最后一个分母
if(better(d)){
memcpy(ans,v,sizeof(LL)*(d+1));
}
return true;
}
bool ok=false;
from=max(from,get_first(aa,bb));//获取那个x
for(LL i=from;;i++){//rd/i<=aa/bb
if((maxd-d+1)*bb<=aa*i)break;//减枝
int flag=0;
for(int ii=0;ii//不能含有被禁止使用地分母
if(br[ii]==i){
flag=1;
break;
}
}
if(flag)continue;
v[d]=i;//aa/bb-1/i
LL ta=aa*i-bb;
LL tb=bb*i;
LL tc=gcd(ta,tb);
ta/=tc;tb/=tc;
if(dfs(d+1,i+1,ta,tb))ok=true;
}
return ok;
}
int main(int argc, char const *argv[])
{
int tim=0,t;
scanf("%d",&t);
while(t--){
int m;
ban.clear();
tot=0;
scanf("%lld%lld%d",&a,&b,&m);
for(int i=0;iscanf("%lld",&x);
br[tot++]=x;
}
for(maxd=1;;maxd++){
memset(ans,-1,sizeof(ans));
if(dfs(0,get_first(a,b),a,b)){
break;
}
}
printf("Case %d: %lld/%lld=",++tim,a,b );
for(int i=0;i<=maxd;++i){
if(i!=maxd) printf("1/%lld+",ans[i] );
else printf("1/%lld\n",ans[i] );
}
}
return 0;
}
紫书例题7-12:Uva1343,见于紫书P210
给你一个如图所示地棋盘,上面有24个格子,每个格子上的数字为1,2或3。你可以向8个方向旋转棋盘,一次移动一格,8个方向为A-H编号。求最小步骤是的中间8个数字相同,若有多个答案输出字典序最小地答案。
还是枚举深度上限maxd,乐观估价函数f^(n)=g^(n)+h^(n)。当前搜索到的深度为g^(n),到目标状态还需要地最小步数为h^(n)。
减枝:如果g^(n)>maxd||g^(n)+h^(n)>maxd,则return;
为了求出字典序最小答案,8个操作从小到大枚举即可。
最后一个问题是怎么求出乐观估价函数中的h^(n):
我们知道中间8个数字是1,2或3。
h^(n)是到目标状态需要地最小步数,那不就很简单了吗?
目标状态是8个数字都相同,你求出同一数字出现最多的次数cnt,h^(n)=8-cnt;
具体看代码中的实现。
#include
using namespace std;
typedef long long LL;
const int N = 1005;
const int center[10]={6,7,8,11,12,15,16,17};//中间8个点的位置
const int reverseOP[10]={5,4,7,6,1,0,3,2};//每种操作的逆操作
const int opt[8][7]={//A-H8种操作影响的位置坐标
{0,2,6,11,15,20,22},//A
{1,3,8,12,17,21,23},//B
{10,9,8,7,6,5,4},//C
{19,18,17,16,15,14,13},//D
{23,21,17,12,8,3,1},//E
{22,20,15,11,6,2,0},//F
{13,14,15,16,17,18,19},//G
{4,5,6,7,8,9,10},//H
};
int mp[25];//地图,每个位置的数字
char ans[N];//操作数答案
bool flag;//是否搜索到
int maxd;//深度上限
inline int getNum(){
int cnt=0;
int num[4]={0};
for(int i=0;i<8;++i){
cnt=max(cnt,++num[mp[center[i]]]);
}
return 8-cnt;
}
void exe(int op){//移动操作
int tmp=mp[opt[op][0]];
for(int i=0;i<6;++i){
mp[opt[op][i]]=mp[opt[op][i+1]];
}
mp[opt[op][6]]=tmp;
}
void dfs(int dep,int fa){
if(flag)return;
if(dep>maxd||dep+getNum()>maxd)return;//减枝
if(getNum()==0){
ans[dep]='\0';
printf("%s\n",ans );
printf("%d\n",mp[center[0]]);
flag=true;
return;
}
for(int i=0;i<8;++i){
if(fa!=-1&&i==reverseOP[fa])continue;
exe(i);
ans[dep]=i+'A';
dfs(dep+1,i);
if(flag)return;
exe(reverseOP[i]);//回溯过程,使用recerseOP数组比较方便
}
}
int main(){
while(~scanf("%d",&mp[0])&&mp[0]){
for(int i=1;i<24;++i){
scanf("%d",&mp[i]);
}
if(getNum()==0){//如果一开始8个数字就相同:
printf("No moves needed\n%d\n",mp[center[0]]);
continue;
}
flag=false;
maxd=1;
while(1){//迭代加深搜索:
dfs(0,-1);
if(flag)break;
maxd++;
}
}
return 0;
}
dfs和bfs是盲目式搜索,而A*和IDA*式启发式搜索。
原因式A*和IDA*式有目的的朝着目标状态搜索,排序了很多没必要的搜索路径。
其精髓就是乐观估价函数f^(n)=g^(n)+h^(n):
g^(n)是搜索到当前状态花费的代价
h^(n)是从当前状态到目标状态需要花费的最小代价。
h(n)是从当前状态到目标状态花费的实际代价。(当然这个不号求出)
显然h^(n)<=h(n)必须成立。
如果要两个状态h1(n)和h2(n)均小于h(n)且h1(n) < h2(n),那么我们要选取的是最接近h(n)的那种情况,也就是h2.(实话,我也不知道为什么,今天才学的IDA*,A*还不会)
听说实现A*算法要用堆,open和close列表,启发式,pre数组记录路径,BFS扩展方式。而且g^(n)跟Dijkstra的松弛一样,需要更新。骚操作太多,学不来。。。