数字游戏

来源:互联网 发布:淘宝纪念币是真的吗 编辑:程序博客网 时间:2024/06/06 19:08

问题描述

http://www.nowcoder.com/questionTerminal/876e3c5fcfa5469f8376370d5de87c06

小易邀请你玩一个数字游戏,小易给你一系列的整数。你们俩使用这些整数玩游戏。每次小易会任意说一个数字出来,然后你需要从这一系列数字中选取一部分出来让它们的和等于小易所说的数字。 例如: 如果{2,1,2,7}是你有的一系列数,小易说的数字是11.你可以得到方案2+2+7 = 11.如果顽皮的小易想坑你,他说的数字是6,那么你没有办法拼凑出和为6 现在小易给你n个数,让你找出无法从n个数中选取部分求和的数字中的最小数。
输入描述:
输入第一行为数字个数n (n ≤ 20)
第二行为n个数xi (1 ≤ xi ≤ 100000)

输出描述:
输出最小不能由n个数选取求和组成的数

输入例子:
3
5 1 2

输出例子:
4

笔记

想到用回溯法,对于每一个数字,要么就是加上这个数字,要么就不加,一共2^n种情况,题目中n<=20好像还行,应该算的过来(代码1)。

但是这种暴力的方法一旦n增大肯定就不行,还是要参考一下大神们的答案。其中一个大神说的特别对。。

听说考试的时候n=100,现在练习改20,不知道什么心态——当时约4000人中有约400人通过了,说明n=100的时候筛选能力就很好啊……

要是100我这种办法也肯定不行了。

代码1,暴力回溯

#include <stdio.h>#include <iostream>#include <vector>#include <set>#include <limits.h>using namespace std;void solve(vector<int> v, set<int> &s, const int &sum, const int i, const int len){        if (i == len)        {                if (s.find(sum) == s.end())                        s.insert(sum);                return;        }        solve(v, s, sum, i+1, len);        solve(v, s, sum+v[i], i+1, len);}int main(){        int n;        while (cin >> n)        {                vector<int> v;                int tmp;                for (int i = 0; i < n; i++)                {                        cin >> tmp;                        v.push_back(tmp);                }                set<int> s;                int sum = 0;                solve(v, s, sum, 0, n);                //for (auto i : s)                //      cout << i << ' ';                //cout << endl;                //int res = 0;                for (int i = 0; i < INT_MAX; i++)                {                               if (s.find(i) == s.end())                        {                                cout << i << endl;                                break;                        }                }        }        return 0;}

代码2,大神解法

Google出过的原题……
http://code.google.com/codejam/contest/4244486/dashboard#s=p2
(需要翻墙查看)
呃,这个题不知道见过几次了,自从google出过一次之后,之后忘了哪里又出了这个弱化版本,现在网易再出一次弱化版本,一点意思都没了……
Google提供的官方题解在:http://code.google.com/codejam/contest/4244486/dashboard#s=a&a=2
中文简单转述:
从小到大排序,一开始一块钱都凑不出来
下面,为了0~x都有,我需要来一个1元的(不然1元凑不出来)
给了你1元的,下面必须给1+1元以内的,不然2元凑不出来
如果再给一个1元的,那你现在能凑出0~2元的,接下来+1+2或者+3,都能增大范围而且不会导致中间缺一个数(4元的不行,因为凑不出3了)
——反正一直往下,直到出现第一个算不出来的值为止。

听说考试的时候n=100,现在练习改20,不知道什么心态——当时约4000人中有约400人通过了,说明n=100的时候筛选能力就很好啊……

#include <stdio.h>#include <ctype.h>#include <string.h>#include <stdlib.h>#include <limits.h>#include <math.h>#include <algorithm>using namespace std;typedef long long ll;int a[1005];int main(){    int miss=0;    int n;    scanf("%d",&n);    for(int i=0;i<n;i++)scanf("%d",&a[i]);    sort(a,a+n);    for(int i=0;i<n;i++){        if(a[i]>miss+1) break;        miss+=a[i];    }    printf("%d\n",miss+1);    return 0;} 

——同学们啊,请一定要做Google出的题目,codejam可能要求高一点(这题是codejam round1的C题),但是至少APAC Test要做一做——你看google前一年出了一个01字典树,微软第二年校招实习笔试也出了一个,现在再加上网易又来一出——简直是校招笔试编程题风向标。

好不容易翻个墙把google的解释拿过来看看。

We will incrementally build a set S of denominations that solves the problem using the minimal number of additional denominations, by restricting ourselves to adding denominations from smallest to largest.

As we add denominations to S, we also maintain an integer N, which is the largest value such that we can produce each value up to and including N. In fact, after all of our choices, S will be able to produce exactly the set of values from 0 to N, and no others.

When we add a new denomination X to S, the new set of values we could produce include each of the values we could produce with the existing set S plus between 0 and C of the new denomination X. If X is at most N+1, then this new set of values will be the set of all values from 0 to N+X*C, so we can update N to N+X*C.

So, we initialize S to the empty set, and N to 0.

Then while N is less than V, we do the following:

  1. Identify the smallest value we cannot produce: N+1.

  2. If there is still a pre-existing denomination which we haven’t used, let the minimum such denomination be X. If X is less than or equal to N+1, we add it to S, and update N to N+X*C.

  3. Otherwise, we have no way yet to produce N+1 using the denominations we have, so we must add to S a new denomination X between 1 and N+1. This will increase N to N+X*C. We use X=N+1. No other choice for X could lead to a better solution, since for X=N+1, the set of values the new S will be able to produce is a superset of the values S would be able to produce with any other choice.

Finally, when we have a set S which can produce all values up to V, we output the number of new denominations we had to add.

In the above algorithm, the first option — using a pre-existing denomination — can only occur D times. When the second option is chosen, N increases to (C+1)N+C. Since we stop when N reaches V, this will occur O(log V) times. So the overall time complexity is O(D+log(V)).

Sample implementation in Java:

import java.util.*;public class C {  public static void main(String[] args) {    Scanner scan = new Scanner(System.in);    int T = scan.nextInt();    for (int TC = 1; TC <= T; TC++) {      int C = scan.nextInt();      int D = scan.nextInt();      int V = scan.nextInt();      Queue<Integer> Q = new ArrayDeque<>();      for (int i = 0; i < D; i++) {        Q.add(scan.nextInt());      }      long N = 0;      int add = 0;      while (N < V) {        // X = The smallest value we cannot produce.        long X = N + 1;        if (!Q.isEmpty() && Q.peek() <= X) {          // Use pre-existing denomination we haven't used.          X = Q.poll();        } else {          // No way to produce N+1, add a new denomination.          add++;        }        N += X * C;      }      System.out.printf("Case #%d: %d\n", TC, add);    }  }}

Vitaliy’s solution in C, which you can download from the scoreboard, is another good example of this approach.

小结

看了这么多,感觉还是用凑钱的例子比较好懂。

其实就是每次判断一下新的数字在不在原来的范围里。
假如原来能表示的范围为1-N,那么如果新的数字小于等于N+1,则都可以“安全地”扩充范围,而不会丢掉范围内任何一个数字。

举个栗子。

假如给的集合是nums = {1, 2, 2, 7}

用N表示目前能表达1-N里所有的数。

一开始什么都没有,能凑出的钱是0,N=0

N = 0。拿到第0个数字nums[0]=1,判断一下新的数字1和原来的范围N+1的大小关系,1<=N+1(1),因此可以“安全地”扩大范围到N = N+1, N=1。也就是说用第1个数字可以表示0-1。

N = 1。再拿到第1个数字nums[1]=2,判断一下新的数字2和原来的范围N+1的大小关系,2<=N+1(2),因此可以“安全地”扩大范围到N = N+2, N=3。也就是说用前两个数字可以表示0-3。

N = 3。再拿到第2个数字nums[1]=2,判断一下新的数字2和原来的范围N+1的大小关系,2<=N+1(4),因此可以“安全地”扩大范围到N = N+2, N=5。也就是说用前两个数字可以表示0-5。

N = 5。再拿到第3个数字nums[3]=7,判断一下新的数字7和原来的范围N+1的大小关系,7>N+1(6),因此不可以“安全地”扩大范围了,也就是说用前三个数字还是只可以表示0-5。

由此,这堆数字只能表示0-5了,不能表示6。

玄机

这里面有什么玄机呢?为什么可以每次判断新的数字和N+1的关系就行?

当1<=x<=N+1的时候:
原来,我们已经可以用前面的一堆数字表示1-N了,现在来了一个新的数字x,在1~N+1的范围内,那么我们现在可以表示1~N+x的范围了。

WHY?

原来是可以表示1~N了,然后考虑N+1。怎么表示N+1?咱们可以用x+(N+1-x)啊!(N+1-x)这个数肯定是在1~N的范围里(注),因此这个数咱们早就可以用前面的数表示出来了。

注:证明:N>=N+1-x>=11<=x<=N ->-1>=-x>=-N->N+1-1>=N+1-x>=N+1-N->N>=N+1-x>=1证毕。

同理,N+x这个数怎么表示?咱们可以用N+x啊!不是说好了N可以用前面的一堆数表示吗,再加上x就好啦!

但是,如果新的数字x>N+1就无能为力了。因为无法表示N+1这个数字。

延伸

由上述的分析过程,我们可以发现,为了用尽可能少的数字来表示尽可能大“不遗漏”的范围,咱们可以每次给原来的集合中增加一个N+1。

举个栗子就是
[1]可以表示1~1。N=1。
增加一个N+1=2。[1, 2]可以表示1~3。N=3。
增加一个N+1=4。[1, 2, 4]可以表示1~7。N=7。
以此类推。

最后上代码

最后我还是根据自己的理解来写一个c的程序来解这道题吧。函数solve1是原来的回溯法,函数solve2是数学分析的方法。经过数学分析,代码竟如此精简高效,简直是欺人太甚。

#include <algorithm>#include <stdio.h>#include <iostream>#include <vector>#include <set>#include <limits.h>using namespace std;void solve(vector<int> v, set<int> &s, const int &sum, const int i, const int len){        if (i == len)        {                if (s.find(sum) == s.end())                        s.insert(sum);                return;        }        solve(v, s, sum, i+1, len);        solve(v, s, sum+v[i], i+1, len);}int solve2(vector<int> v){        sort(v.begin(), v.end());        //for (auto i: v)        //        cout << i << ' ';        //cout << endl;        int N = 0;        for (auto x : v)        {                if (x <= N+1)                        N += x;                else                        break;        }        return N+1; }int main(){        int n;        while (cin >> n)        {                vector<int> v;                int tmp;                for (int i = 0; i < n; i++)                {                        cin >> tmp;                        v.push_back(tmp);                }                // solve1                //set<int> s;                //int sum = 0;                //solve(v, s, sum, 0, n);                //for (auto i : s)                //      cout << i << ' ';                //cout << endl;                //int res = 0;                //for (int i = 0; i < INT_MAX; i++)                //{                     //      if (s.find(i) == s.end())                //      {                //              cout << i << endl;                //              break;                //      }                //}                //                // solve2                cout << solve2(v) << endl;        }        return 0;}
0 0
原创粉丝点击