// 2D to 1D Maze Checker.
//
// Instructions:
//
// The maze checker accepts as input a 2D traditional maze,
// followed by a 1D maze. An example input is:
//
// +-+-+-+-+-+-+
// | | | |
// + + + + +-+-+
// | | | |
// +-+-+ +-+-+ +
// | |
// +-+-+-+ +-+ +
// | | |
// + +-+-+-+ + +
// | | | |
// +-+-+-+-+-+-+
// | D| D E|G E F| F | G |
//
// If you compile this checker on your own system, the maze checker
// will also accept as an argument the name of a file containing
// both mazes. If you have two arguments, the checker will use
// the first as the filename for the 2D maze, and the second as
// the filename for the 1D maze.
//
// If you compile this on your own system and run it in say bash,
// you can link this up directly to your program for a maze.
// To use it this way, use a piped redirect as your second argument,
// like so:
//
// ./a.exe mazefilename <(./runmyprogram)
//
// ...where mazefilename contains a 2D maze, and your program simply
// outputs to stdout the 1D maze.
//
// This checker is intentionally non-golfy; if you can follow it
// feel free to steal some algorithm hints. However, it doesn't
// actually do anything to make 1D mazes...
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <utility>
#include <iterator>
#include <unordered_set>
// XXX XXX XXX
// Uncomment this for canonicalization diagnostics
//#define DIAGNOSTICS
using namespace std;
vector<string> tr;
unordered_set<unsigned> visited;
// True iff c is a valid wall character
bool wall(char c) {
switch (c) {
case '+':
case '-':
case '|':
return true;
}
return false;
}
// True iff c is a valid space character
bool space(char c) {
if (c==' ') return true;
return false;
}
// Convert an ofset to a maze-character
char ofs2mc(unsigned int o) {
if (o<26) return 'A'+o;
o-=26;
if (o<26) return 'a'+o;
return 0;
}
// Convert a maze-character to an offset
unsigned mc2ofs(char c) {
if (c>='A'&&c<='Z') return c-'A';
if (c>='a'&&c<='z') return 26+c-'a';
return 255;
}
// True iff c is a valid warp character
bool warpchar(char c) {
return mc2ofs(c)<255;
}
// True iff r is a row consisting only of a wall
bool solidrow(const string& r) { return r.find_first_not_of("+-|")==string::npos; }
// True iff r has +'s for all lattice points.
bool latticed(const string& r) {
for (unsigned i=0, e=r.size(); i<e; i+=2) if (!wall(r[i])) return false;
return true;
}
// True iff r has internal spaces as required for non-latticed rows
bool spaced(const string& r) {
for (unsigned i=1, e=r.size(); i<e; i+=2) if (!space(r[i])) return false;
return true;
}
// Adds row if valid, returns true if valid, and sets finished to true
// if valid, if this is not the first row, and if it's a wall.
// If invalid, returns false.
bool add(string& row, bool &finished) {
if (!row.empty()) {
if (row[row.size()-1]=='\r') row.resize(row.size()-1);
}
if (row.empty()) return false;
if (row.size()<4) return false;
if (0==row.size()%2) return false;
if (tr.empty()) {
if (!solidrow(row)) return false;
if (!latticed(row)) return false;
tr.emplace_back("");
tr.back().swap(row);
return true;
}
if (0==(tr.size()%2)) {
if (!latticed(row)) return false;
tr.push_back(row);
if (solidrow(row)) return finished=true;
return true;
}
if (row[0]!='|') return false;
if (row[row.size()-1]!='|') return false;
if (!spaced(row)) return false;
tr.push_back(row);
return true;
}
// Generic graph structure
struct Graph {
// Stores the edges in the graph.
// The n-th entry is node n. Each char in the string
// represents an edge to n.
std::vector<string> edges;
unsigned makenode() {
if (edges.size()>=52) return 53;
unsigned rv = edges.size();
edges.push_back(string());
return rv;
}
void addEdge(unsigned e1, unsigned e2) {
edges[e1].push_back(ofs2mc(e2));
edges[e2].push_back(ofs2mc(e1));
}
void showGraph() {
for (unsigned i=0, e=edges.size(); i<e; ++i) {
const string& edgelist = edges[i];
for (unsigned int j=0, je=edgelist.size(); j<je; ++j) {
if (mc2ofs(edgelist[j])<i) continue;
cout << ofs2mc(i) << " - " << edgelist[j] << "\n";
}
}
}
};
// Scan a maze searching for any dead end at all.
// We don't need to path search here; we know an end when
// we see one, and left to right/top to bottom search suffices.
bool findanend(unsigned &x, unsigned& y) {
const unsigned width = tr[0].size();
const unsigned height = tr.size();
for (y=1; y<height; y+=2) {
for (x=1; x<width; x+=2) {
bool left = (tr[y][x-1]==' ');
bool up = (tr[y-1][x]==' ');
bool right = (tr[y][x+1]==' ');
bool down = (tr[y+1][x]==' ');
if (left+up+right+down==1) return true;
}
}
return false;
}
// Creates a graph out of the maze in (global) tr.
//
// Directions:
// 1
// 0 x 2
// 3
bool makegraph(Graph& g, unsigned x=1, unsigned y=1, int fromdir=-1, int fromnode=-1) {
unsigned edges=0;
if (fromdir<0) {
// This algorithm requires starting with a location that's a node
// in order to correctly hook the first found endpoints.
//
// Finding an end is the easiest way to prime the pump for this.
if (!findanend(x,y)) { cerr << "cannot find an end" << endl; return false; }
}
if (visited.count((x<<16)|y)) { cerr << "already visited " << x << "," << y << endl; return false; }
visited.insert((x<<16)|y);
bool left = (fromdir-0) && (tr[y][x-1]==' ');
bool up = (fromdir-1) && (tr[y-1][x]==' ');
bool right = (fromdir-2) && (tr[y][x+1]==' ');
bool down = (fromdir-3) && (tr[y+1][x]==' ');
bool from = (fromdir+1);
int nextnode = fromnode;
switch (edges = left+up+right+down+from) {
case 1:
case 3:
case 4:
nextnode = g.makenode();
tr[y][x]=ofs2mc(nextnode);
if (nextnode>52) { cerr << "node limit exceeded" << endl; return false; }
if (fromnode>=0) g.addEdge(fromnode, nextnode);
}
if (left ) if (!makegraph(g, x-2, y , 2, nextnode)) return false;
if (up ) if (!makegraph(g, x , y-2, 3, nextnode)) return false;
if (right) if (!makegraph(g, x+2, y , 0, nextnode)) return false;
if (down ) if (!makegraph(g, x , y+2, 1, nextnode)) return false;
return true;
}
bool testallvisited() {
const unsigned width = tr[0].size();
const unsigned height = tr.size();
for (unsigned y=1; y<height; y+=2) {
for (unsigned x=1; x<width; x+=2) {
if (!visited.count((x<<16)|y)) return false;
}
}
return true;
}
// Join (concatenate) all strings from begin to end.
template<typename I> string join(I begin, I end) {
string rv;
while (begin!=end) {rv.append(*begin); ++begin;}
return rv;
}
// Canonicalize the graphs. Note that this canonicalization algorithm
// is tailored for the types of graphs we care about (namely, connected
// graphs with no cycles; aka "unrooted trees").
string canonicalize(const Graph& graph) {
// A replacement to perform
struct Replacement {
unsigned at;
char var;
string with;
#ifdef DIAGNOSTICS
void prn() {
cerr << ":" << ofs2mc(at) << ":s/" << var << "/" << with << "/" << endl;
}
#endif
};
// A "canon" represents a canonical form for a subset of the graph
// built onto a node. Not until we connect the final edges does
// this canon actually represent the graph.
//
// There is a "Canon" per node; which we'll just call the canon
// for that node.
struct Canon {
// The "canon" for each edge of this node.
// Initially, this is just the name of the far node;
// with respect to the canon we consider this a "variable".
vector<string> eds;
#ifdef DIAGNOSTICS
void prn() {
cerr << "{";
for (unsigned i=0,e=eds.size(); i<e; ++i)
cerr << " {" << eds[i] << "}";
cerr << " }" << endl;
}
#endif
void add(const string& nodelist) {
eds.resize(nodelist.size());
for (unsigned i=0,e=eds.size(); i<e; ++i)
eds[i]=nodelist.substr(i,1);
}
unsigned size() const { return eds.size(); }
// Count the variables in this Canon
unsigned varct() const {
unsigned rv=0;
for (unsigned i=0,e=size(); i<e; ++i)
if (eds[i].size()==1) rv+=warpchar(eds[i][0]);
return rv;
}
// Returns a canonical substitution for a single variable.
string substitution() {
if (varct()!=1) throw 2;
sort(eds.begin(), eds.end());
return join(eds.begin()+1, eds.end());
}
// Returns a full canon. This is used once we run out
// of variables to replace. _Each_ full canon represents
// the entire graph; _one_ of them (lexically chosen)
// is the graph's canonical form.
string canon() {
if (varct()) throw 2;
sort(eds.begin(),eds.end());
return join(eds.begin(), eds.end());
}
// Apply a replacement to a node.
bool apply(const Replacement& r) {
for (unsigned i=0,e=size(); i<e; ++i)
if ((eds[i].size()==1)&&(eds[i][0]==r.var)) {
eds[i]=string("}")+r.with+string("{");
return true;
}
return false;
}
};
vector<Canon> es(graph.edges.size());
unsigned const gsize=graph.edges.size();
unsigned int maxedges=0;
for(unsigned i=0; i<gsize; ++i) {
const string& er = graph.edges[i];
if (er.size()>maxedges) maxedges=er.size();
if (maxedges>4) return "";
es[i].add(er);
}
string rv;
unordered_set<unsigned> cullset;
for (unsigned varNodesLeft=es.size();varNodesLeft;) {
varNodesLeft=0;
vector<Replacement> rlist;
#ifdef DIAGNOSTICS
cerr << "------- loop" << endl;
cerr << gsize << "/" << cullset.size() << endl;
#endif
// Cycle 1: Find all substitutables...
// these are nodes with only one edge left as a variable
// Ignore nodes with no variables,
// form replacements for nodes with one variable,
// and count all other nodes
for (unsigned i=0; i<gsize; ++i) {
if (cullset.count(i)) continue;
Canon& node = es[i];
switch (node.varct()) {
case 0:
#ifdef DIAGNOSTICS
cerr << ofs2mc(i) << ": 0 size :"; node.prn();
#endif
continue;
case 1:
{
#ifdef DIAGNOSTICS
cerr << ofs2mc(i) << ": 1 size :"; node.prn();
#endif
cullset.insert(i);
string nodecanon = node.substitution();
rlist.emplace_back(
Replacement{
mc2ofs(node.eds[0][0]),
ofs2mc(i),
nodecanon
});
#ifdef DIAGNOSTICS
rlist.back().prn();
#endif
continue;
}
default:
#ifdef DIAGNOSTICS
cerr << ofs2mc(i) << ": 2* size :"; node.prn();
#endif
++varNodesLeft;
}
}
if (rlist.empty()) {
// cannot proceed; something's off
if (varNodesLeft) {
#ifdef DIAGNOSTICS
cerr << "ABORT" << endl;
#endif
return "";
}
#ifdef DIAGNOSTICS
cerr << "END_OF_SUBS" << endl;
#endif
break;
}
rv.clear();
for (unsigned i=0, e=rlist.size(); i<e; ++i) {
auto rentry=rlist[i];
auto & rnode=es[rentry.at];
rnode.apply(rentry);
#ifdef DIAGNOSTICS
cerr << "[*" << ofs2mc(rentry.at) << "]: "; rnode.prn();
#endif
//if (varNodesLeft) continue;
if (rnode.varct()) continue;
string thisc = rnode.canon();
#ifdef DIAGNOSTICS
cerr << "considering " << thisc;
#endif
if (thisc>rv) {
rv=thisc;
#ifdef DIAGNOSTICS
cerr << "...preferred" << endl;
} else {
cerr << "...rejected for " << rv << endl;
#endif
}
}
#ifdef DIAGNOSTICS
cerr << "-- var nodes " << varNodesLeft << endl;
cerr << "rv candidate: " << rv << endl;
#endif
}
for_each(rv.begin(),rv.end(),[](char&c){
c=(c-'{')?'(':')';
});
#ifdef DIAGNOSTICS
cerr << "Canonical:" << rv << endl;
#endif
return rv;
}
int main(int argc, const char* argv[])
{
string row;
bool finished=false;
unique_ptr<ifstream> pif;
istream* in = &cin;
if (argc>1) {
pif.reset(new ifstream(argv[1]));
in = pif.get();
}
while (!finished
&& getline(*in, row)
&& add(row, finished))
{
}
if (!finished) {
cerr << "Not a valid maze" << endl;
cerr << tr.size() << " lines read" << endl;
return 1;
}
Graph tmg;
if (!makegraph(tmg)) {
cerr << "Not a compliant maze" << endl;
return 1;
}
if (!testallvisited()) {
cerr << "Not a compliant maze (not all visited)" << endl;
return 1;
}
// XXX XXX XXX
// Uncomment if you want to see your maze with node labels
// copy(tr.begin(), tr.end(), ostream_iterator<string>(cout,"\n"));
// XXX XXX XXX
// Uncomment if you want to see your graph in edge form
// tmg.showGraph();
string canonicalTM = canonicalize(tmg);
row.clear();
if (argc>2) {
pif.reset(new ifstream(argv[2]));
in = pif.get();
}
while (getline(*in, row)) {
if (row.empty()) continue;
// Allow allows a "->" between the mazes
if (row.find_first_of(">")!=string::npos) continue;
// Allow blank lines with whitespace between mazes
if (row.find_first_not_of(" \t\r\n")==string::npos) continue;
break;
}
if (row.empty()) {
cerr << "1D Maze not found" << endl;
return 1;
}
if (row[row.size()-1]=='\r') row.resize(row.size()-1);
if (0==row.size()%2) {
cerr << "Not a valid 1D maze; width must be odd (mazes built on lattice points)" << endl;
return 1;
}
if ((row[0]!='|')||(row[row.size()-1]!='|')) {
cerr << "1D maze error: No outer walls" << endl;
return 1;
}
for (unsigned i=0, e=row.size(); i<e; i+=2) {
if ((row[i]!='|')&&(row[i]!=' ')) {
cerr
<< row << "\n"
<< setw(i+1) << "^" << "\n"
<< "Lattice point must be wall or space" << endl;
return 1;
}
}
{
unsigned ct[52]={};
for (unsigned i=0, e=row.size(); i<e; ++i) {
switch (row[i]) {
case '|':
case ' ':
continue;
default:
break;
}
if (warpchar(row[i])) {
if (++ct[mc2ofs(row[i])]>2) {
cerr << "Warp point " << row[i] << " can only appear twice" << endl;
return 1;
}
}
}
for (unsigned i=0; i<52; ++i) {
if (ct[i]==2) continue;
if (ct[i]==0) continue;
cerr << "Warp point " << ofs2mc(i) << " must appear twice" << endl;
return 1;
}
}
Graph w1dgraph;
unsigned warps[52]; fill(warps, warps+52, 52);
int from=-1;
for (unsigned i=0;;++i) {
unsigned ndx=i*2+1;
if (ndx>=row.size()) break;
unsigned gsize = w1dgraph.edges.size();
int to = 0;
char p=row[ndx];
if (warpchar(p)) {
if (warps[mc2ofs(p)]==52) {
to = w1dgraph.makenode();
warps[mc2ofs(p)] = to;
} else {
to = warps[mc2ofs(p)];
}
} else if (p==' ') {
to = w1dgraph.makenode();
} else throw 1;
if ((from>=0)&&(row[ndx-1]==' ')) {
w1dgraph.addEdge(from,to);
}
from=to;
}
// XXX XXX XXX
// Uncomment if you want to see your 1D maze's graph once relabeled
string canonical1D = canonicalize(w1dgraph);
if (canonicalTM != canonical1D) {
cerr
<< "This doesn't look right...\n"
<< "The traditional maze's graph has the canonical form:\n"
<< " " << canonicalTM << "\n"
<< "But the 1D graph's canonical form is:\n"
<< " " << canonical1D << endl;
return 1;
}
cout
<< "Success! Both mazes match.\n"
<< "The resulting canonical form graph is:\n"
<< canonicalTM << endl;
}