猜猜子集策略

时间:2019-05-02 17:45:14

标签: algorithm

猜猜子集游戏

游戏的工作原理是这样的。您与对手对抗。

设置

  • 有一组固定的数字 A (例如{1、2,...,30})
  • 对手通过删除最多 k 个元素(例如0-5)来选择一个秘密子集 S

客观

您知道 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>


想法

  • 如果 k = A的大小,我认为我们比一次猜测一个元素要好。即{1},{2},{3},...,{30},因为每个元素的包含都彼此独立。上面的示例中有30个猜测。
  • 如果 k = 1 ,我们可以先猜测 A 以查看是否未删除任何元素,然后通过二进制搜索找到丢失的元素。在上面的示例中,这大约有6个猜测。
  • 我仍在努力迷惑 k = 2
  • 似乎有点像see this puzzling.stackexchange question已解决的策划者,但在策划者中,对手会告诉您您有多少猜测是正确的,而不仅仅是“是/否”。
  • 顺便说一句,我在尝试为一个软件模块更新30个库依赖项时遇到了这个问题。更新所有组件后,构建失败,但是我强烈怀疑不会超过5个组件导致构建失败。 (在这种情况下,我从日志中获得了比哪个No失败更多的信息,而单个No则没有帮助,但这仍然是一个漫长的过程,因为每次猜测测试大约需要5-7分钟。)

1 个答案:

答案 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;
}