游戏的工作原理是这样的。您与对手对抗。
设置
客观
您知道 A 和 k ,您的工作是通过猜测来找出子集。
猜测
您猜到了一个子集 B 。
对手会告诉您是,如果B⊆S(即,您猜到的所有元素是否在秘密子集 S 中)否则为否。
问题
您可以使用哪种策略找出最少猜测的子集?
可播放的版本 您可以在这里玩游戏。选择A和k,您可以进行猜测。当您以为自己想出了秘密时就可以揭开它的秘密。重新运行该代码段以重试。
// Random Integer in [min, max)
const getRandomInt = (min, max) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
};
class GuessSubsetGame extends React.Component {
constructor(props) {
super(props);
this.state = {
numGuesses: 0,
guess: '',
prevGuesses: [],
revealed: false
};
}
isValid = (guess) => guess
.split(',')
.map(val => val.trim())
.every(element => this.props.secret.includes(element)) ? 'Yes' : 'No';
onSubmit = (e) => {
this.setState((state, props) => ({
guess: '',
numGuesses: state.numGuesses + 1,
prevGuesses: [{guess: state.guess, valid: this.isValid(state.guess)}, ...state.prevGuesses]
}));
e.preventDefault();
}
renderPrevGuesses() {
return this.state.prevGuesses.map(({guess, valid}) => {
return (
<div>
{valid} - {guess}
</div>
);
});
};
render() {
return (
<div>
{this.state.revealed ? 'Secret was ' + this.props.secret : <button onClick={() => this.setState({revealed: true})} >Reveal Secret</button> }
<div>
{'Guess Number: ' + this.state.numGuesses}
</div>
<form onSubmit={this.onSubmit}>
<div>
<label for="guess">Guess a Subset (like 1,2,3): </label>
<input
type="text"
name="guess"
id="guess"
value={this.state.guess}
onChange={(e) => this.setState({guess: e.target.value})} />
</div>
<div>
<input type="submit" value="Guess" />
</div>
</form>
<div>
<div>History</div>
{this.renderPrevGuesses()}
</div>
</div>
);
}
}
class GuessSubsetGameCreator extends React.Component {
constructor(props) {
super(props);
this.state = {
isBuilt: false,
A: '1,2,3,4,5,6',
k: 2,
secret: null
};
};
onSubmit = (e) => {
const actualK = getRandomInt(0, parseInt(this.state.k) + 1);
const aAsArray = this.state.A.split(',');
const shuffled = aAsArray.sort(() => 0.5 - Math.random());
const secret = shuffled.slice(0, shuffled.length - actualK).sort();
this.setState({
isBuilt: true,
secret
});
e.preventDefault();
};
render() {
let result;
if (!this.state.isBuilt) {
return (
<form onSubmit={this.onSubmit}>
<div>
<label for="A">What are the elements of A? </label>
<input
type="text"
name="A"
id="A"
required
value={this.state.A}
onChange={(e) => this.setState({A: e.target.value})} />
</div>
<div>
<label for="k">How many elements can be removed? </label>
<input
type="number"
name="k"
id="k"
required
value={this.state.k}
onChange={(e) => this.setState({k: e.target.value})} />
</div>
<div>
<input type="submit" value="Start Game" />
</div>
</form>
);
}
else {
return <GuessSubsetGame A={this.state.A} secret={this.state.secret} />
}
return result;
}
}
ReactDOM.render(
<GuessSubsetGameCreator />,
document.getElementById("root")
);
div {
padding-top: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
想法
答案 0 :(得分:1)
以下C ++程序将报告最佳策略的长度,该长度是根据我在评论中提到的minimax游戏树策略的一般性计算得出的,并使用动态编程来提高速度。在此策略下,它可以有选择地输出跟踪,以显示最大长度的查询序列。 (输出整个树并不难,但是由于它可能是几乎完整的,具有20多个级别的二叉树,因此对于人类来说并不是很有趣。)
从本质上讲,即使我们允许涉及多个早期查询元素的查询,也可以简洁地表示关于所有元素 的完整知识状态。知识状态在程序中表示为向量,其中第一个元素是k,第二个元素是A中我们一无所知的项目数(我在评论中忘记了状态的这一方面),其余全部向量元素是A的不相交“标记”子集的大小,以递增顺序列出。 “标记”子集是我们知道错过S的至少一个元素的子集。知识状态向量始终被规范化(请参见normalise()
函数)。相同的矢量表示用于表示“玩家移动”(从A的每个不相交子集中选择一些元素),即使在这种情况下第一个元素(其中k表示为无意义)也是如此。>
令人惊讶的是,发现存在最坏情况的查询序列,这些查询序列导致知识状态序列中的循环-也就是说,在最坏情况下,查询序列的长度无限长。幸运的是,可以调整DP来解决此问题。
要运行该程序,请指定k作为第一个命令行参数,并指定| A |。作为第二。要在最佳策略下添加回溯以显示特定的最大长度查询序列,请将-t
作为第一个参数。
例如,运行fewest_tests -t 5 30
会产生:
Minimum number of queries required in the worst case: 20
One possible maximum-length sequence of queries that follow an optimal strategy:
Height=20. Knowledge state: [5 30]
Choose [0 3]: Adversary replies YES. (Other answer also leads to height-19 subtree.)
Height=19. Knowledge state: [5 27]
Choose [0 3]: Adversary replies YES. (Other answer also leads to height-18 subtree.)
Height=18. Knowledge state: [5 24]
Choose [0 3]: Adversary replies YES. (Other answer also leads to height-17 subtree.)
Height=17. Knowledge state: [5 21]
Choose [0 2]: Adversary replies YES. (Other answer also leads to height-16 subtree.)
Height=16. Knowledge state: [5 19]
Choose [0 2]: Adversary replies YES. (Other answer also leads to height-15 subtree.)
Height=15. Knowledge state: [5 17]
Choose [0 2]: Adversary replies YES. (Other answer also leads to height-14 subtree.)
Height=14. Knowledge state: [5 15]
Choose [0 2]: Adversary replies YES. (Other answer also leads to height-13 subtree.)
Height=13. Knowledge state: [5 13]
Choose [0 1]: Adversary replies YES. (Other answer leads to height-11 subtree.)
Height=12. Knowledge state: [5 12]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-11 subtree.)
Height=11. Knowledge state: [5 11]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-10 subtree.)
Height=10. Knowledge state: [5 10]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-9 subtree.)
Height=9. Knowledge state: [5 9]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-8 subtree.)
Height=8. Knowledge state: [5 8]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-7 subtree.)
Height=7. Knowledge state: [5 7]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-6 subtree.)
Height=6. Knowledge state: [5 6]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-5 subtree.)
Height=5. Knowledge state: [5 5]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-4 subtree.)
Height=4. Knowledge state: [5 4]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-3 subtree.)
Height=3. Knowledge state: [5 3]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-2 subtree.)
Height=2. Knowledge state: [5 2]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-1 subtree.)
Height=1. Knowledge state: [5 1]
Choose [0 1]: Adversary replies YES. (Other answer also leads to height-0 subtree.)
Height=0. Knowledge state: [5 0]
这在我的笔记本电脑上耗时约16 s,并使用约110 Mb。请注意,我需要增加默认的堆栈大小(在MSVC ++上为/L
);默认值为1Mb,可以解决而不会崩溃的最大问题是5 23
。为了安全起见,我将其增加到512Mb。
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <set>
#include <map>
#include <cassert>
using namespace std;
bool verbose = false;
//DEBUG: Enable output of vectors
template <typename T>
ostream& operator<<(ostream& os, vector<T>& v) {
os << '[';
for (unsigned i = 0; i < v.size(); ++i) {
if (i > 0) {
os << ' ';
}
os << v[i];
}
os << ']';
return os;
}
// Call f with every valid choice of items.
template <typename F>
void for_each_choice(vector<unsigned>& v, F f, vector<unsigned>& u, bool takenSomethingYet) {
unsigned i = u.size();
if (i == v.size()) {
// We have chosen amounts to take from every subset.
if (takenSomethingYet) {
if (verbose) cerr << "From " << v << " we have chosen " << u << "." << endl; //DEBUG
f(u);
}
} else {
// "+ (i == 1)": Only try choosing all the items in a subset for the unknown subset, v[1].
// "(i <= 2 ... u[i - 1])": For a sequence of equal-size subsets, only choose a nondecreasing sequence of item counts (symmetry).
u.push_back(0);
for (unsigned j = (i <= 2 || v[i] != v[i - 1] ? 0 : u[i - 1]); j < v[i] + (i == 1); ++j) {
// Try choosing j items from this set
u.back() = j;
for_each_choice(v, f, u, takenSomethingYet);
takenSomethingYet = true;
}
u.pop_back();
}
}
// For convenience.
template <typename F>
void for_each_choice(vector<unsigned>& v, F f) {
vector<unsigned> u(1, 0); // First position, where k would be, is ignored
for_each_choice(v, f, u, false);
}
void normalise(vector<unsigned>& r) {
if (verbose) cerr << "normalise(" << r << ") called." << endl; //DEBUG
// Get rid of all subsets of size 0 or 1.
unsigned j = 2;
for (unsigned i = 2; i < r.size(); ++i) {
if (r[i] <= 1) {
r[0] -= r[i]; // Decrease k whenever we have a singleton marked subset: its single member must be marked
} else {
r[j++] = r[i];
}
}
r.erase(r.begin() + j, r.end());
// If the maximum possible number of marked elements are already accounted for by disjoint marked subsets, then all "unknown" elements
// are really known not to be marked.
if (r.size() == r[0] + 2) {
r[1] = 0;
}
sort(r.begin() + 2, r.end());
if (verbose) cerr << "Result of normalise(): " << r << "." << endl; //DEBUG
}
// Return configuration resulting from the adversary saying "yes" to the choice u
vector<unsigned> yes(vector<unsigned> v, vector<unsigned> u) {
if (verbose) cerr << "From " << v << ", answer YES to " << u << "." << endl; //DEBUG
vector<unsigned> r = v;
for (unsigned i = 1; i < v.size(); ++i) {
r[i] -= u[i];
}
normalise(r);
return r;
}
// Return configuration resulting from the adversary saying "no" to the choice u
// Note that, because normalise() sets v[1] to 0 if all marked elements are already exhausted by marked disjoint subsets, this function can never
// produce a contradictory knowledge state (that is, one with more than v[0] disjoint marked subsets).
vector<unsigned> no(vector<unsigned> v, vector<unsigned> u) {
if (verbose) cerr << "From " << v << ", answer NO to " << u << "." << endl; //DEBUG
vector<unsigned> r = v;
r.push_back(0);
for (unsigned i = 1; i < v.size(); ++i) {
if (u[i] > 0) {
r[1] += v[i] - u[i]; // Tricky: In every subset that we chose some elements from, every *unchosen* element goes back in the "we dunno" pile.
r[i] -= v[i]; // For all i >= 2, this will set r[i] to 0, and this element will be deleted by normalise()
r.back() += u[i];
}
}
normalise(r);
return r;
}
// First element is k, the maximum number of marked items.
// Second element is number of items about which we know nothing.
// Remaining items are in sorted order, and are the sizes of disjoint subsets of items: we know that each of these subsets contains at least one marked item.
map<vector<unsigned>, int> memo_; // Holds solutions already computed
map<vector<unsigned>, vector<unsigned>> nextMove_; // Holds a best move for already-computed solutions
unsigned solve(vector<unsigned> v) {
if (verbose) cerr << "solve(" << v << ") called." << endl; //DEBUG
auto us = memo_.find(v);
if (us != memo_.end() && (*us).second == -1) {
if (verbose) cerr << "solve(" << v << ") returning " << (UINT_MAX - 1) << " because the current state already appeared in the history, indicating an infinite loop." << endl; //DEBUG
return UINT_MAX - 1; // -1 so that it survives the +1 it gets later
}
// Because v has been normalised, we have identified all marked states iff there are no unknown elements and no marked subsets.
if (v.size() == 2 && v[1] == 0) {
if (verbose) cerr << "solve(" << v << ") returning 0 since no unknown elements could be marked." << endl; //DEBUG
return 0;
}
if (us == memo_.end()) {
// Haven't computed the solution to this subproblem yet: do it now
us = memo_.insert(make_pair(v, -1)).first; // Tell descendant calls that this state is in the history and would lead to a worst-case infinite loop
// Generate every possible successor state
unsigned best = UINT_MAX;
vector<unsigned> bestMove;
for_each_choice(v, [&](vector<unsigned> u) {
unsigned cur = 1U + max(solve(yes(v, u)), solve(no(v, u)));
if (cur < best) {
best = cur;
bestMove = u;
}
});
(*us).second = best;
nextMove_[v] = bestMove;
}
if (verbose) cerr << "solve(" << v << ") returning " << (*us).second << "." << endl; //DEBUG
return (*us).second;
}
// You must call solve() first to populate nextMove_[].
void traceBack(vector<unsigned> v) {
cout << "One possible maximum-length sequence of queries that follow an optimal strategy:\n";
while (true) {
unsigned iSteps = solve(v);
vector<unsigned> u = nextMove_[v];
cout << "Height=" << iSteps << ". Knowledge state: " << v << endl;
if (iSteps == 0) {
break;
}
cout << "Choose " << u << ": Adversary replies ";
unsigned hYes = solve(yes(v, u));
unsigned hNo = solve(no(v, u));
if (hYes == iSteps - 1) {
cout << "YES. (Other answer " << (hYes == hNo ? "also " : "") << "leads to height-" << hNo << " subtree.)\n";
v = yes(v, u);
} else {
assert(hNo == iSteps - 1);
cout << "NO. (Other answer " << (hYes == hNo ? "also " : "") << "leads to height-" << hNo << " subtree.)\n";
v = no(v, u);
}
}
}
int main(int argc, char** argv) {
int nArgs = 0;
bool showTraceback = false;
if (string(argv[1]) == "-v") {
verbose = true;
++nArgs;
}
if (string(argv[nArgs + 1]) == "-t") {
showTraceback = true;
++nArgs;
}
unsigned k = atoi(argv[nArgs + 1]);
unsigned n = atoi(argv[nArgs + 2]);
vector<unsigned> v(2);
v[0] = k;
v[1] = n;
normalise(v); // Needed in case k=0.
cout << "Minimum number of queries required in the worst case: " << solve(v) << endl;
if (showTraceback) {
traceBack(v);
}
return 0;
}