181. 回转游戏
题目链接:回转游戏
算法分析:
要保证估价函数小于等于未来实际值,只能以最理性状态分析。对于每个状态的中间8个格子,假设出现次数最多的数字为k,其余非k的数字要变成k,最少需要操作8 - k次。未来实际值不会少于这个次数。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int b[8][8], d[25], szmove[1000010];
int anstep, ansval;
inline int g()
{
int f[4];
memset(f, 0, sizeof(f));
++f[b[3][3]]; ++f[b[3][4]]; ++f[b[3][5]];
++f[b[4][3]]; ++f[b[4][5]];
++f[b[5][3]]; ++f[b[5][4]]; ++f[b[5][5]];
if (f[1] > f[2] && f[1] > f[3]) return 8 - f[1];
if (f[2] > f[1] && f[2] > f[3]) return 8 - f[2];
if (f[3] > f[1] && f[3] > f[2]) return 8 - f[3];
}
inline bool isok()
{
if (b[3][3] == b[3][4] && b[3][4] == b[3][5] && b[3][5] == b[4][3] && b[4][3] == b[4][5] && b[4][5] == b[5][3] && b[5][3] == b[5][4] && b[5][4] == b[5][5]) return 1;
return 0;
}
bool dfs(int step, int maxdep, int dir) // dir:上一步的操作编号
{
// cout<<"step="<<step<<endl;
// cout<<g()<<endl;
if (step + g() > maxdep) return 0;
if (isok())
{
anstep = step;
ansval = b[3][3];
return 1;
}
// A
if (dir != 6)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[1][3];
for (int i = 2; i <= 7; ++i) b[i-1][3] = b[i][3];
b[7][3] = t;
szmove[step+1] = 1;
if (dfs(step + 1, maxdep, 1)) return 1;
memcpy(b, tem, sizeof(b));
}
// B
if (dir != 5)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[1][5];
for (int i = 2; i <= 7; ++i) b[i-1][5] = b[i][5];
b[7][5] = t;
szmove[step+1] = 2;
if (dfs(step + 1, maxdep, 2)) return 1;
memcpy(b, tem, sizeof(b));
}
// C
if (dir != 8)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[3][7];
for (int j = 6; j >= 1; --j) b[3][j+1] = b[3][j];
b[3][1] = t;
szmove[step+1] = 3;
if (dfs(step + 1, maxdep, 3)) return 1;
memcpy(b, tem, sizeof(b));
}
// D
if (dir != 7)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[5][7];
for (int j = 6; j >= 1; --j) b[5][j+1] = b[5][j];
b[5][1] = t;
szmove[step+1] = 4;
if (dfs(step + 1, maxdep, 4)) return 1;
memcpy(b, tem, sizeof(b));
}
// E
if (dir != 2)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[7][5];
for (int i = 6; i >= 1; --i) b[i+1][5] = b[i][5];
b[1][5] = t;
szmove[step+1] = 5;
if (dfs(step + 1, maxdep, 5)) return 1;
memcpy(b, tem, sizeof(b));
}
// F
if (dir != 1)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[7][3];
for (int i = 6; i >= 1; --i) b[i+1][3] = b[i][3];
b[1][3] = t;
szmove[step+1] = 6;
if (dfs(step + 1, maxdep, 6)) return 1;
memcpy(b, tem, sizeof(b));
}
// G
if (dir != 4)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[5][1];
for (int j = 2; j <= 7; ++j) b[5][j-1] = b[5][j];
b[5][7] = t;
szmove[step+1] = 7;
if (dfs(step + 1, maxdep, 7)) return 1;
memcpy(b, tem, sizeof(b));
}
// H
if (dir != 3)
{
int tem[8][8];
memcpy(tem, b, sizeof(b));
int t = b[3][1];
for (int j = 2; j <= 7; ++j) b[3][j-1] = b[3][j];
b[3][7] = t;
szmove[step+1] = 8;
if (dfs(step + 1, maxdep, 8)) return 1;
memcpy(b, tem, sizeof(b));
}
return 0;
}
int main()
{
while (1)
{
scanf("%d", &d[1]);
if (!d[1]) break;
for (int i = 2; i <= 24; ++i) scanf("%d", &d[i]);
b[1][3] = d[1];
b[1][5] = d[2];
b[2][3] = d[3];
b[2][5] = d[4];
b[3][1] = d[5];
b[3][2] = d[6];
b[3][3] = d[7];
b[3][4] = d[8];
b[3][5] = d[9];
b[3][6] = d[10];
b[3][7] = d[11];
b[4][3] = d[12];
b[4][5] = d[13];
b[5][1] = d[14];
b[5][2] = d[15];
b[5][3] = d[16];
b[5][4] = d[17];
b[5][5] = d[18];
b[5][6] = d[19];
b[5][7] = d[20];
b[6][3] = d[21];
b[6][5] = d[22];
b[7][3] = d[23];
b[7][5] = d[24];
int maxdep = 0;
// szmove[0] = 0;
anstep = 0;
while (1)
{
if (dfs(0, maxdep, 0))
{
if (anstep == 0) printf("No moves needed\n");
else
{
for (int i = 1; i <= anstep; ++i)
{
if (szmove[i] == 1) printf("A");
if (szmove[i] == 2) printf("B");
if (szmove[i] == 3) printf("C");
if (szmove[i] == 4) printf("D");
if (szmove[i] == 5) printf("E");
if (szmove[i] == 6) printf("F");
if (szmove[i] == 7) printf("G");
if (szmove[i] == 8) printf("H");
}
printf("\n");
}
printf("%d\n", ansval);
break;
}
++maxdep;
}
}
return 0;
}
总结与反思:
1.数组b不能直接当做dfs的参数传递,因为数组是传址调用,会在后续的递归操作中改变b的值。因此,每次赋值给tem,操作后再反赋值回来给b,不会造成b发生变化。
2.迭代加深因为有步数限制,所以,可以不用标记,因为步数很浅。如果步数深,必须得用标记了。但步数深,也就不用迭代加深了。
180. 排书
题目链接:180. 排书
算法分析:
难点还是估价函数。在既定状态下,假设第i本书后是第i + 1本书,称i + 1是i的正确后继。每个状态下,假设所有书的错误后继总数为tot。每操作一次,最多改变3本书的错误后继,理想状态就是这3本书的错误后继都能修正。这样,该状态最少需要操作(tot + 2) / 3次。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <ctime>
using namespace std;
#define ull unsigned long long
const int p = 2887777;
int d, n, b[20];
ull szfirst[2900000], szdata[1000000], sznext[1000000], cnt;
inline bool isok(int tem[])
{
for (int i = 1; i < n; ++i) if (tem[i] + 1 != tem[i+1]) return 0;
return 1;
}
inline void biaoji(int b[])
{
ull sum = 0;
for (int i = 1; i <= n; ++i) sum = sum * 16 + b[i];
int u = sum % p;
++cnt;
szdata[cnt] = sum;
sznext[cnt] = szfirst[u];
szfirst[u] = cnt;
}
inline int fnd(int b[])
{
ull sum = 0;
for (int i = 1; i <= n; ++i) sum = sum * 16 + b[i];
int u = sum % p;
int d = szfirst[u];
while (d)
{
if (szdata[d] == sum) return 1;
d = sznext[d];
}
return 0;
}
inline void szdel(int b[])
{
ull sum = 0;
for (int i = 1; i <= n; ++i) sum = sum * 16 + b[i];
int u = sum % p;
int d = szfirst[u];
if (szdata[d] == sum)
{
szfirst[u] = sznext[d]; return;
}
int pre = d;
while (d)
{
if (szdata[d] == sum) break;
pre = d;
d = sznext[d];
}
sznext[pre] = sznext[d]; // 删掉编号为d的结点
}
bool dfs(int b[], int dep)
{
//统计错误后继个数
int tot = 0;
for (int i = 1; i < n; ++i) if (b[i] + 1 != b[i+1]) ++tot;
if (dep - 1 + ceil(tot / 3.0) > d) return 0;
// cout<<"dep-1……="<<dep - 1 + ceil(tot / 3.0)<<" d="<<d<<endl;
if(isok(b))
{
// for (int i = 1; i <= n; ++i) cout<<b[i]<<" "; cout<<endl;
// cout<<"dep="<<dep<<endl;
// getchar(); getchar();
return 1;
}
for (int len = 1; len < n; ++len) // 枚举将要移动的区间长度
for (int i = 1; i <= n; ++i) // 枚举区间左端点
{
int j = i + len - 1; // 区间右端点
if (j > n) continue;
int bb[20], tem[20];
int s = 0;
for (int k = 1; k <= i -1; ++k) bb[++s] = b[k];
for (int k = j + 1; k <= n; ++k) bb[++s] = b[k];
// cout<<"dep="<<dep<<" d="<<d<<" len="<<len<<" i="<<i<<" j="<<j<<endl;
// getchar(); getchar();
// 枚举插入点 :插在最前面
s = 0;
for (int k = i; k <= j; ++k) tem[++s] = b[k];
for (int k = 1; k <= n - (j - i) -1; ++k) tem[++s] = bb[k];
// cout<<"i到j:";for (int k = i; k <= j; ++k) cout<<b[k]<<" "; cout<<endl;
// for (int k = 1; k <= n; ++k) cout<<tem[k]<<" "; cout<<endl;
// if (dep == 1 && d == 2 && i == 3 && j == 5)
// {
// getchar(); getchar();
// }
/*
if (b[1] == 2 && b[2] == 1 && b[3] == 3 && b[4] == 4 && b[5] == 5)
{
cout<<"2 1 3 4 5"<<endl;
cout<<"dep="<<dep<<endl;
getchar(); getchar();
}
*/
if (!fnd(tem))
{
biaoji(tem);
if (dfs(tem, dep + 1)) return 1;
szdel(tem);
}
// 枚举其他插入点
for (int t = 1; t <= n - (j - i) -1; ++t) // 插到每个点的后面
{
s = 0;
for (int k = 1; k <= t; ++k) tem[++s] = bb[k];
for (int k = i; k <= j; ++k) tem[++s] = b[k];
for (int k = t + 1; k <= n - (j - i) -1; ++k) tem[++s] = bb[k];
if (!fnd(tem))
{
biaoji(tem);
if (dfs(tem, dep + 1)) return 1;
szdel(tem);
}
}
}
return 0;
}
int main() // 过三个点,其余点超时
{
double st = clock();
int T;
scanf("%d", &T);
while (T--)
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &b[i]);
if (isok(b))
{
printf("0\n"); continue;
}
d = 0;
while (1)
{
++d; // d是搜索深度
cnt = 0;
memset(szfirst, 0, sizeof(szfirst));
// memset(sznext, 0, sizeof(sznext));
// memset(szdata, 0, sizeof(szdata));
biaoji(b);
if (dfs(b, 1) || d >= 5) break;
szdel(b);
}
if (d < 5) printf("%d\n", d);
else printf("5 or more\n");
}
double ed = clock();
printf("%.0lf\n", ed - st);
return 0;
}
总结与反思:
1.本来想着进行标记,状态也复杂,就想用哈希表。结果发现,标记用时比不标记还长,得不偿失。大段代码都是标记,没派上用场。
2.选取某一段进行随机插入是操作难点,一不小心就会超时。由于运动是相对的,因此,只要将某一段向后插入即可。代码中的实现比较精妙。