//------------------------------------------------------------------------------
/// @file
/// @author ハル研究所プログラミングコンテスト実行委員会
///
/// @copyright Copyright (c) 2018 HAL Laboratory, Inc.
/// @attention このファイルの利用は、同梱のREADMEにある
/// 利用条件に従ってください。
//------------------------------------------------------------------------------
#include "Answer.hpp"
#include <random>
int stage = 0;
//------------------------------------------------------------------------------
/// ハッシュ値の計算に使用するランダムな64ビットの値。2^Bits(=1024)個。
/// 適度にランダムであれば何でもいいけど、mt19937_64 で生成してみる。
class Hash {
public:
static const int Bits = 10;
static uint64_t Table[1 << Bits];
public:
Hash() {
std::mt19937_64 rand;
for (int i = 0; i < 1 << Bits; i++) {
Table[i] = rand();
}
}
};
uint64_t Hash::Table[1 << Hash::Bits];
Hash RandomTableInitializer;
//------------------------------------------------------------------------------
/// 3次元ベクトル。
struct Vector3i {
public:
int X;
int Y;
int Z;
Vector3i() {
X = 0;
Y = 0;
Z = 0;
}
Vector3i(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
int Weight() {
return X * Y * (Z + 10);
}
bool operator==(Vector3i rhs) {
if (X == rhs.X && Y == rhs.Y && Z == rhs.Z) {
return true;
}
else {
return false;
}
}
};
//------------------------------------------------------------------------------
/// 生地の位置とサイズ。
/// 生地は位置、サイズともに3次元ベクトルで表現する。
/// 位置のzはオーブンに入れる時間。
/// サイズのzは焼けるまでの所要時間。
/// これで、20*20*1000の箱詰め問題となる。
/// (同時に2つ焼けないので、2つ以上の位置のzが同じにならないように注意する必要がある)
struct Box {
public:
Vector3i Position;
Vector3i Size;
Box() {
}
Box(Vector3i position, Vector3i size) {
Position = position;
Size = size;
}
public:
/// 2つの生地が衝突しているかを判定する。
bool Collided(Box& target) {
if (Position.X + Size.X <= target.Position.X) {
return false;
}
if (target.Position.X + target.Size.X <= Position.X) {
return false;
}
if (Position.Y + Size.Y <= target.Position.Y) {
return false;
}
if (target.Position.Y + target.Size.Y <= Position.Y) {
return false;
}
if (Position.Z + Size.Z <= target.Position.Z) {
return false;
}
if (target.Position.Z + target.Size.Z <= Position.Z) {
return false;
}
return true;
}
bool operator==(Box& rhs) {
if (Position == rhs.Position && Size == rhs.Size) {
return true;
}
else {
return false;
}
}
/// この生地の位置とサイズのハッシュ値。
uint64_t GetHashCode() {
uint64_t hash = Position.X + (Position.Y << 5);
hash = Hash::Table[hash & ((1 << Hash::Bits) - 1)] ^ (hash >> Hash::Bits);
hash += Position.Z + 256;
hash = Hash::Table[hash & ((1 << Hash::Bits) - 1)] ^ (hash >> Hash::Bits);
hash += Size.X + (Size.Y << 5);
hash = Hash::Table[hash & ((1 << Hash::Bits) - 1)] ^ (hash >> Hash::Bits);
hash += Size.Z;
hash = Hash::Table[hash & ((1 << Hash::Bits) - 1)] ^ (hash >> Hash::Bits);
hash = Hash::Table[hash & ((1 << Hash::Bits) - 1)] ^ (hash >> Hash::Bits);
return hash;
}
};
//------------------------------------------------------------------------------
/// 時間経過も込みで管理するオーブン。
class Oven3D {
public:
static const int PieceCapacity = 100;
static const int ZMax = 2000;
hpc::Array<Box, PieceCapacity> Pieces;
/// 格納済みのBoxを配置の時間が早いものから順に並べたときのインデックス。
int ZOrders[PieceCapacity];
/// その時間に置く生地があるかどうか。(同時に2つ焼けないため)
bool Busy[ZMax];
uint64_t HashCode;
Oven3D()
{
for (int i = 0; i < ZMax; i++) {
Busy[i] = false;
}
HashCode = 0;
}
public:
/// 1ターン進める。
/// Position.Z から1引くだけ。
/// Position.Z が負のときは、前のターンまでに置いた生地。
/// Position.Z + Size.Z が 0 になると、焼き上がり。
void Update() {
for (int i = 0; i < Pieces.count(); i++) {
auto& piece = Pieces[ZOrders[i]];
HashCode ^= piece.GetHashCode();
if (piece.Position.Z + piece.Size.Z <= 0) {
RemoveAt(ZOrders[i]);
i--;
}
else {
int z = piece.Position.Z;
if (z >= 0) {
#if _DEBUG
HPC_ASSERT(Busy[z] == true);
#endif
Busy[z] = false;
}
piece.Position.Z--;
z--;
if (z >= 0) {
#if _DEBUG
HPC_ASSERT(Busy[z] == false);
#endif
Busy[z] = true;
}
HashCode ^= piece.GetHashCode();
}
}
}
/// 配置済みの生地との衝突判定。
bool Collided(Box& c) {
for (int i = 0; i < Pieces.count(); i++) {
if (Pieces[i].Collided(c)) {
return true;
}
}
return false;
}
/// オーブンに生地を追加する。
void Add(Box& c) {
#if _DEBUG
HPC_ASSERT(Busy[c.Position.Z] == false);
#endif
Busy[c.Position.Z] = true;
Pieces.add(c);
int count = Pieces.count();
ZOrders[count - 1] = count - 1;
for (int i = count - 1; i > 0; i--) {
if (Pieces[ZOrders[i]].Position.Z < Pieces[ZOrders[i - 1]].Position.Z) {
int t = ZOrders[i];
ZOrders[i] = ZOrders[i - 1];
ZOrders[i - 1] = t;
}
else {
break;
}
}
HashCode ^= c.GetHashCode();
}
/// オーブンから生地を取り除く。
void RemoveAt(int index) {
#if _DEBUG
HPC_ASSERT(index >= 0 && index < Count());
#endif
int z = Pieces[index].Position.Z;
if (z >= 0) {
#if _DEBUG
HPC_ASSERT(Busy[z] == true);
#endif
Busy[z] = false;
}
for (int i = 0; i < Pieces.count(); i++) {
if (ZOrders[i] == index) {
for (int j = i; j < Pieces.count() - 1; j++) {
ZOrders[j] = ZOrders[j + 1];
}
break;
}
}
for (int i = 0; i < Pieces.count(); i++) {
if (ZOrders[i] > index) {
ZOrders[i]--;
}
}
HashCode ^= Pieces[index].GetHashCode();
Pieces.erase(index);
}
int Count() {
return Pieces.count();
}
/// 指定したサイズの生地を置くことができる、最も小さいZを座標ごとに求める。
/// オーブンを3次元の箱と見立てて、配置しようとしている生地が入る場所を求める。
/// 戻り値は共有バッファ SharedZS に書き込まれる。
int SharedZS[hpc::Parameter::OvenHeight][hpc::Parameter::OvenWidth];
void CalcVacantZs(Vector3i size, bool zFirst) {
int sh = hpc::Parameter::OvenHeight + 1 - size.Y;
int sw = hpc::Parameter::OvenWidth + 1 - size.X;
int minZ = 0;
for (int i = 0; ; i++) {
if (Busy[i]) {
minZ++;
}
else {
break;
}
}
for (int i = 0; i < sh; i++) {
for (int j = 0; j < sw; j++) {
SharedZS[i][j] = minZ;
}
}
bool minZFound = false;
for (int pi = 0; pi < Pieces.count() && minZFound == false; pi++) {
auto& piece = Pieces[ZOrders[pi]];
int ymin = piece.Position.Y - size.Y + 1;
int ymax = piece.Position.Y + piece.Size.Y;
if (ymin < 0) {
ymin = 0;
}
if (ymax > sh) {
ymax = sh;
}
int xmin = piece.Position.X - size.X + 1;
int xmax = piece.Position.X + piece.Size.X;
if (xmin < 0) {
xmin = 0;
}
if (xmax > sw) {
xmax = sw;
}
for (int i = ymin; i < ymax; i++) {
for (int j = xmin; j < xmax; j++) {
if (SharedZS[i][j] + size.Z <= piece.Position.Z ||
piece.Position.Z + piece.Size.Z <= SharedZS[i][j]) {
if (zFirst) {
if (SharedZS[i][j] + size.Z <= piece.Position.Z) {
minZFound = true;
}
}
}
else {
SharedZS[i][j] = piece.Position.Z + piece.Size.Z;
while (Busy[SharedZS[i][j]]) {
SharedZS[i][j]++;
}
}
}
}
}
}
/// オーブンに配置されている生地全体のハッシュ値。
/// 各生地のハッシュ値をxorしたもの。
uint64_t GetHashCode() {
return HashCode;
}
void Dump() {
std::printf("----------------------------------------\n");
for (int i = 0; i < Pieces.count(); i++) {
std::printf("(%d, %d, %d), (%d, %d, %d)\n", Pieces[i].Position.X, Pieces[i].Position.Y, Pieces[i].Position.Z, Pieces[i].Size.X, Pieces[i].Size.Y, Pieces[i].Size.Z);
}
std::printf("----------------------------------------\n");
}
};
//------------------------------------------------------------------------------
/// Actionを13ビットの値で表現したもの。
struct CompactAction {
public:
static const int Wait = 8191;
static int FromXYPieceID(int x, int y, int pieceID) {
#if _DEBUG
HPC_ASSERT(x >= 0 && x < hpc::Parameter::OvenWidth);
HPC_ASSERT(y >= 0 && y < hpc::Parameter::OvenHeight);
HPC_ASSERT(pieceID >= 0 && pieceID < 2 * hpc::Parameter::CandidatePieceCount);
#endif
return ((y * hpc::Parameter::OvenWidth + x) << 4) | pieceID;
}
static int X(int image) {
return (image >> 4) % hpc::Parameter::OvenWidth;
}
static int Y(int image) {
return (image >> 4) / hpc::Parameter::OvenWidth;
}
static int PieceID(int image) {
return image & 15;
}
};
//------------------------------------------------------------------------------
/// 探索結果の値と、そのときの行動。
/// スコアは、小さい方がいいので、ペナルティという呼び方をする。
/// なお、ペナルティは、レーンで待機中の全ての生地を配置したときの、生地のペナルティの合計。
/// 生地のペナルティは、体積と、配置する時間(1ターン増えるごとに1増える)をかけた値。
struct SearchResult {
public:
int Penalty;
int CompactAction;
SearchResult(int penalty, int compactAction)
{
Penalty = penalty;
CompactAction = compactAction;
}
};
//------------------------------------------------------------------------------
/// 探索結果のキャッシュ。
/// Searchで探索した結果を保存しておく。
/// いわゆるDPのテーブルのようなものだが、全部のオーブンの状態を保存するメモリはないので、
/// オーブンのハッシュ値でキャッシュする。
/// オーブンのハッシュ値のうち、下位14ビットとその上の32ビットの合計46ビット、
/// および使用済み生地が一致するものは、同じノードとみなす。
/// (もしかしたら違うかもしれないけど、非常に確率が低いのと、探索を少し間違える
/// だけで、大きな問題はないと思うので気にしない)
/// ハッシュ値のうち、下位14ビットは、テーブルのどこに入るかで決まるので保存しない。
struct CacheNode {
public:
uint32_t MiniHash;
uint16_t Used;
uint16_t CompactAction;
int Value;
CacheNode() {
MiniHash = 0;
Used = 0;
CompactAction = 0;
Value = 0;
}
CacheNode(uint32_t miniHash, uint16_t used, uint16_t compactAction, int value) {
MiniHash = miniHash;
Used = used;
CompactAction = compactAction;
Value = value;
}
};
//------------------------------------------------------------------------------
/// 探索結果のキャッシュ。
/// ハッシュテーブルのサイズは2^14個(16384個)。
class Cache {
public:
static const int TagBitLength = 14;
static const int BlockCapacity = 200;
static hpc::Array<CacheNode, BlockCapacity> SearchResultCache[1 << TagBitLength];
static void AddResultCache(SearchResult result, uint64_t hash, int used) {
int tag = hash & ((1 << TagBitLength) - 1);
uint32_t miniHash = (uint32_t)(hash >> TagBitLength);
auto& cacheBlock = SearchResultCache[tag];
uint16_t used16 = (uint16_t)used;
for (int i = 0; i < cacheBlock.count(); i++) {
if (cacheBlock[i].MiniHash == miniHash && cacheBlock[i].Used == used16) {
if (cacheBlock[i].Value != result.Penalty ||
cacheBlock[i].CompactAction != result.CompactAction) {
#if _DEBUG
std::printf("Cache hash conflicts. %d, %08x\n", tag, miniHash);
HPC_ASSERT(false);
#endif
}
return;
}
}
if (cacheBlock.count() < BlockCapacity - 1) {
cacheBlock.add(CacheNode(miniHash, used16, (uint16_t)result.CompactAction, result.Penalty));
}
}
static CacheNode* FindResultCache(uint64_t hash, int used) {
int tag = hash & ((1 << TagBitLength) - 1);
uint32_t miniHash = (uint32_t)(hash >> TagBitLength);
auto& cacheBlock = SearchResultCache[tag];
uint16_t used16 = (uint16_t)used;
for (int i = 0; i < cacheBlock.count(); i++) {
if (cacheBlock[i].MiniHash == miniHash && cacheBlock[i].Used == used16) {
return &(cacheBlock[i]);
}
}
return nullptr;
}
};
hpc::Array<CacheNode, Cache::BlockCapacity> Cache::SearchResultCache[1 << Cache::TagBitLength];
//------------------------------------------------------------------------------
/// ソルバー。
class Solver {
public:
int Frame;
Oven3D CurrentOven;
Solver()
{
Frame = 0;
Box ceil(Vector3i(0, 0, hpc::Parameter::GameTurnLimit + 1), Vector3i(hpc::Parameter::OvenWidth, hpc::Parameter::OvenHeight, 1));
CurrentOven.Add(ceil);
}
public:
/// 指定した生地を置くべき位置。
/// なるべく早くおけるところを優先し、置くタイミングが同じなら、外周に近いところを優先する。
Box PutPos(Vector3i& piece) {
const int PenaltyMax = 100000000;
CurrentOven.CalcVacantZs(piece, true);
Box maxCol;
int minPenalty = PenaltyMax;
for (int i = 0; i < hpc::Parameter::OvenHeight + 1 - piece.Y; i++) {
for (int j = 0; j < hpc::Parameter::OvenWidth + 1 - piece.X; j++) {
int yDist = std::min(i, hpc::Parameter::OvenHeight - piece.Y - i);
int xDist = std::min(j, hpc::Parameter::OvenWidth - piece.X - j);
int penalty = xDist + yDist + std::min(xDist, yDist) * 100 + CurrentOven.SharedZS[i][j] * 10000;
if (penalty < minPenalty) {
minPenalty = penalty;
maxCol = Box(Vector3i(j, i, CurrentOven.SharedZS[i][j]), piece);
}
}
}
return maxCol;
}
/// オーブンのペナルティ。なるべく広く開いているものがペナルティが小さくなる。
/// その方が、将来、置く場所の選択肢が増えるため。
/// (ただし、これを入れてもあまり変わらないのと、処理時間がかかるので、今は使ってない)
int OvenPenalty() {
int sizex = 5;
int sizey = 5;
int sizez = 100;
int penalty = 0;
CurrentOven.CalcVacantZs(Vector3i(sizex, sizey, sizez), false);
for (int i = 0; i < hpc::Parameter::OvenHeight + 1 - sizey; i++) {
for (int j = 0; j < hpc::Parameter::OvenWidth + 1 - sizex; j++) {
penalty += CurrentOven.SharedZS[i][j];
}
}
return penalty;
}
/// 生地のペナルティ。
/// 生地の体積と、配置する時間(1ターン遅くなるごとに1増える)をかけた値。
/// つまり、大きいものを後に置くほどペナルティが大きくなる。
int PiecePenalty(Box piece) {
int s = piece.Position.Z * piece.Size.Z;
s *= piece.Size.X * piece.Size.Y;
return s;
}
/// 現在のオーブンに、残りの生地を全部置いたときのペナルティを求める。
/// 生地を置く場所は、その生地にとって最善の位置(なるべく早く置けて、なるべく端に近いところ)とする。
/// 置き方を決める順番をいろいろ変えてみて、最もペナルティの小さいものを採用する。
/// (置く順番ではなく、置く場所と時間を決める順番なので注意!)
/// usedの各ビットで、その生地をすでに置いているかどうかを示す。
/// depthは、探索の深さ。depthが2以上なら次に置く生地を残りの生地すべてで試す。
/// depthが1なら、残りの中から10個ぐらいで試す。
/// depthが0なら、残りの生地を大きいものから順に置いて試す。
/// countは残り個数。
/// limitは、ペナルティがこの値と等しい、または大きくなることが確定したときに、探索を中止する値。
/// そのときは、戻り値として、limitに等しい値、行動はWaitを返す。
SearchResult Search(hpc::Array<Vector3i, 2 * hpc::Parameter::CandidatePieceCount>& pieces,
int used,
int depth,
int count,
int limit
) {
// limitが0または負のときは、探索しなくてもいい。
if (limit <= 0) {
return SearchResult(limit, CompactAction::Wait);
}
// 計算済みのものがキャッシュされていたら、それを返す。
uint64_t hash = CurrentOven.GetHashCode();
CacheNode* cacheNode = Cache::FindResultCache(hash, used);
if (cacheNode != nullptr) {
return SearchResult(cacheNode->Value, cacheNode->CompactAction);
}
SearchResult minResult(limit, CompactAction::Wait);
for (int pi = 0; pi < pieces.count(); pi++) {
if (((used & (1 << pi))) != 0) {
continue;
}
auto& piece = pieces[pi];
Box p = PutPos(piece);
CurrentOven.Add(p);
int penalty;
if (count == 1) {
penalty = 0; // OvenPenalty();
penalty += PiecePenalty(p);
CurrentOven.RemoveAt(CurrentOven.Count() - 1);
if (penalty > limit) {
penalty = limit;
return SearchResult(penalty, CompactAction::Wait);
}
auto result = SearchResult(penalty, p.Position.Z == 0 ? CompactAction::FromXYPieceID(p.Position.X, p.Position.Y, pi) : CompactAction::Wait);
Cache::AddResultCache(result, hash, used);
return result;
}
else {
int lpenalty = PiecePenalty(p);
if (limit - lpenalty <= 0) {
CurrentOven.RemoveAt(CurrentOven.Count() - 1);
return SearchResult(limit, CompactAction::Wait);
}
SearchResult result = Search(pieces, (used | (1 << pi)), depth - 1, count - 1, minResult.Penalty - lpenalty);
result.Penalty += lpenalty;
CurrentOven.RemoveAt(CurrentOven.Count() - 1);
if (result.Penalty < minResult.Penalty) {
minResult = result;
if (p.Position.Z == 0) {
minResult.CompactAction = CompactAction::FromXYPieceID(p.Position.X, p.Position.Y, pi);
}
}
if (depth <= 0) {
Cache::AddResultCache(minResult, hash, used);
return minResult;
}
if (depth == 1 && pi >= 10) {
Cache::AddResultCache(minResult, hash, used);
return minResult;
}
}
}
Cache::AddResultCache(minResult, hash, used);
return minResult;
}
int NextAction(hpc::Array<Vector3i, 2 * hpc::Parameter::CandidatePieceCount>& pieces) {
Frame++;
for (int i = 0; i < 1 << Cache::TagBitLength; i++) {
Cache::SearchResultCache[i].clear();
}
CurrentOven.Update();
int pieceOrders[2 * hpc::Parameter::CandidatePieceCount];
for (int i = 0; i < 2 * hpc::Parameter::CandidatePieceCount; i++) {
pieceOrders[i] = i;
}
// 大きい生地ほど、早く配置を決められるように、並べ替え。
for (int i = 2 * hpc::Parameter::CandidatePieceCount - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (pieces[j].Weight() < pieces[j + 1].Weight()) {
auto t = pieces[j];
pieces[j] = pieces[j + 1];
pieces[j + 1] = t;
int to = pieceOrders[j];
pieceOrders[j] = pieceOrders[j + 1];
pieceOrders[j + 1] = to;
}
}
}
SearchResult result = Search(pieces, 0, 2, 2 * hpc::Parameter::CandidatePieceCount, 1000000000);
int a = result.CompactAction;
if (a != CompactAction::Wait) {
auto box = Box(Vector3i(CompactAction::X(a), CompactAction::Y(a), 0), pieces[CompactAction::PieceID(a)]);
CurrentOven.Add(box);
// 並びを元に戻す
int originalAction = CompactAction::FromXYPieceID(CompactAction::X(a), CompactAction::Y(a), pieceOrders[CompactAction::PieceID(a)]);
return originalAction;
}
else {
return a;
}
}
};
Solver* solver;
//------------------------------------------------------------------------------
namespace hpc {
//------------------------------------------------------------------------------
/// コンストラクタ。
/// @detail 最初のステージ開始前に実行したい処理があればここに書きます。
Answer::Answer()
{
}
//------------------------------------------------------------------------------
/// デストラクタ。
/// @detail 最後のステージ終了後に実行したい処理があればここに書きます。
Answer::~Answer()
{
}
//------------------------------------------------------------------------------
/// 各ステージ開始時に呼び出される処理。
/// @detail 各ステージに対する初期化処理が必要ならここに書きます。
/// @param aStage 現在のステージ。
void Answer::init(const Stage& aStage)
{
solver = new Solver();
}
//------------------------------------------------------------------------------
/// このターンでの行動を指定する。
/// @detail 希望する行動を Action の static 関数で生成して return してください。
/// 正常ではない行動を return した場合、何も起きません。
/// 詳細は Action のヘッダを参照してください。
/// @param aStage 現在のステージ。
Action Answer::decideNextAction(const Stage& aStage)
{
Array<Vector3i, 2 * Parameter::CandidatePieceCount> pieces;
for (int i = 0; i < 2; i++) {
auto laneType = i == 0 ? hpc::CandidateLaneType_Large : hpc::CandidateLaneType_Small;
for (int j = 0; j < Parameter::CandidatePieceCount; j++) {
auto& piece = aStage.candidateLane(laneType).pieces()[j];
pieces.add(Vector3i(piece.width(), piece.height(), piece.requiredHeatTurnCount() + 1));
}
}
int next = solver->NextAction(pieces);
if (next == CompactAction::Wait) {
return Action::Wait();
}
else {
int pieceID = CompactAction::PieceID(next);
auto laneType = pieceID / hpc::Parameter::CandidatePieceCount == 0 ? CandidateLaneType_Large : hpc::CandidateLaneType_Small;
int piece = pieceID % hpc::Parameter::CandidatePieceCount;
return Action::Put(laneType, piece, Vector2i(CompactAction::X(next), CompactAction::Y(next)));
}
}
//------------------------------------------------------------------------------
/// 各ステージ終了時に呼び出される処理。
/// @detail 各ステージに対する終了処理が必要ならここに書きます。
/// @param aStage 現在のステージ。
void Answer::finalize(const Stage& aStage)
{
delete solver;
stage++;
}
} // namespace
// EOF