I D A ∗ IDA^* IDA∗
前置芝士:迭代加深
有这样一类题,每次我们可以对对象进行某些操作,求将对象变成目标所需最小操作数.
我们可以考虑深度优先搜索和广度优先搜索.
深度优先搜索的策略是每次在搜索树上选定一个分支,然后从该分支逐渐深入往下扩展,直至搜索至目标答案或边界时回溯.这种无差别式的搜索策略具有一定的缺陷:假若我们的目标在搜索树另一分支上较浅的层次,但我们却沿着选择的错误分支深入往下扩展,毋庸置疑会增加许多搜索量.
为解决此问题,我们可以考虑对搜索进行改造。
仍然按照原策略在搜索树上进行扩展,但是进行多次搜索.每次限定一个搜索层数,达到限定层时无论是否找到答案都不再继续扩展。若未找到目标,则将限定层数增加 1 1 1然后重新搜索.
用这种方法虽然看似具有许多重复计算量,但跟原先埋头往下搜,搜至深层次时冗余的无效计算,可以算是小巫见大巫了.
例题见下方埃及分数.
I D A ∗ IDA^* IDA∗
在迭代加深的基础上,我们引入估价函数 f ( x ) f(x) f(x)(估价函数了解详见 A*算法浅谈).仍然满足 f ( x ) ≤ g ( x ) f(x)\leq g(x) f(x)≤g(x),其中 g ( x ) g(x) g(x)代表从当前状态 x x x到达目标状态所需的实际最小代价.若当前迭代剩余层数为 n o w now now,则若 f ( x ) > n o w f(x)>now f(x)>now,必有 g ( x ) ≥ f ( x ) > n o w g(x)\geq f(x)>now g(x)≥f(x)>now,故 x x x必定不可能在本次迭代中搜索至答案,因此可以提前排除.
同样的, I D A ∗ IDA^* IDA∗算法的难点在于估价函数的设计,我们进一步在题目中感受.
P1763 埃及分数
题意: 详见链接
思路: 考虑到最终答案需要满足分解个数最小,因此可以用迭代加深.
由于选取的单位分数不能相同,因此我们可以按单位分数从大到小,即分母从小到大开始枚举.
考虑剪枝减少搜索量,在每一层枚举中,若剩余分数为 d c \frac{d}{c} cd,上次枚举分母选取 p r e pre pre,那么这一层枚举起点就是 p r e + 1 pre+1 pre+1,枚举终点设为 p p p,迭代剩余层数为 l e n len len.若本层选取了 p p p,那么本层及接下来所有层次的单位分数之和必定小于 m x = l e n p mx=\frac{len}{p} mx=plen,若 m x < = d c mx<=\frac{d}{c} mx<=cd,那即使接下来全部选取最大单位分数也无法凑齐 d c \frac{d}{c} cd,于是有 m x = l e n p > d c mx=\frac{len}{p}>\frac{d}{c} mx=plen>cd,亦即 p < ⌈ l e n ⋅ c d ⌉ p<\lceil \frac{len\cdot c}{d}\rceil p<⌈dlen⋅c⌉,以此作为枚举终点 e d ed ed即可.
code
const ll N=100000;
ll a, b;
ll _stack[N], _t;
ll ans[N];
bool flag;
ll goal;
inline void dfs(ll C, ll D, ll pre, ll now) {
if (C<0 || D<0) return;
ll gcd=__gcd(C, D);
C/=gcd, D/=gcd;
if (now==goal) {
if (C!=1) return;
if (D==_stack[_t]) return;
if (flag && D>=ans[goal]) return;
for (R ll i=1; i<goal; i++) ans[i]=_stack[i];
ans[goal]=D;
flag=true;
return;
}
ll end=ceil((double)D*(goal-now+1)/C);
for (R ll i=pre+1; i<=end; i++) {
_stack[++_t]=i;
dfs(C*i-D, D*i, i, now+1);
--_t;
}
}
int main() {
read(a); read(b);
if (a==1) {
writeln(b);
return 0;
}
while (!flag) {
++goal;
dfs(a, b, 0, 1);
}
for (R ll i=1; i<=goal; i++) writesp(ans[i]); putchar('\n');
}
题意:
有 n ( n ≤ 16 ) n(n\leq 16) n(n≤16)个铁盘从上到下叠放,大小各不相同。我们每次可以选取最上面若干个铁盘进行翻转(如 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4翻转为 4 , 3 , 2 , 1 4,3,2,1 4,3,2,1).问最少翻转几次可以实现铁盘从上到下按从小到大排列.
思路:
首先进行离散化,因此最终需要序列满足 n u m [ i ] = i num[i]=i num[i]=i
观察最终序列,我们考虑每相邻两层间的关系,满足 n u m [ i + 1 ] − n u m [ i ] = i + 1 − i = 1 num[i+1]-num[i]=i+1-i=1 num[i+1]−num[i]=i+1−i=1
我们思考每次操作对序列带来的影响。若选取第 k k k层,实际上对 1 t o k 1 \ to\ k 1 to k的影响仅是翻转,相邻差不变,但却改变了 k + 1 k+1 k+1与 k k k间的差值。因此可以得出一个结论,每次操作至多改变一个差值. 然后目标状态为 n u m [ i + 1 ] − n u m [ i ] = 1 num[i+1]-num[i]=1 num[i+1]−num[i]=1,因此我们可以统计当前 a b s ( n u m [ i + 1 ] − n u m [ i ] ) ≠ 1 abs(num[i+1]-num[i])\not=1 abs(num[i+1]−num[i])=1的 i i i个数,作为状态估价函数 f ( x ) f(x) f(x).
然后进行剪枝,显然如果两次操作选择同一位置是无意义的,因此记录前驱 p r e pre pre排雷。
code
const ll N=17;
ll n;
ll num[N], rem[N];
const ll f() {
ll sum=0;
for (R ll i=1; i<n; i++) {
sum+=(abs(num[i]-num[i+1])!=1);
}
return sum;
}
bool flag;
inline void dfs(ll x, ll pre) {
if (x==0) {
for (R ll i=1; i<=n; i++) {
if (num[i]!=i) return;
}
flag=true;
return;
}
if (flag) return;
ll end=n;
while (num[end]==end) --end;
for (R ll i=2; i<=end; i++) {
if (i==pre) continue;
ll l=1, r=i;
while (l<r) {
swap(num[l], num[r]);
++l; --r;
}
if (f()<=x) dfs(x-1, i);
l=1, r=i;
while (l<r) {
swap(num[l], num[r]);
++l; --r;
}
}
}
int main () {
read(n);
for (R ll i=1; i<=n; i++) read(num[i]), rem[i]=num[i];
sort(rem+1, rem+n+1);
for (R ll i=1; i<=n; i++) {
num[i]=lower_bound(rem+1, rem+n+1, num[i])-rem;
}
ll goal=0;
while (!flag) {
++goal;
dfs(goal, 0);
}
writeln(goal);
}