Google Kick Start 2019 Round G
Problem A - Book Reading
题目描述
有一本页的书,其中缺了页,分别为。 现在有个读者,其中的第个读者只会阅读页码数为整数倍的那些页(缺失的页除外)。 请统计所有读者阅读的页数总和。
数据范围: 。
题解
我们需要的实际上是一个数组cnt[N+1],用于记录从1到N每个数字的整数倍中,缺页的数量。求得cnt后,只需要统计即可。
思路1 穷举倍数
用一个哈希表torn[N+1]记录第页是否损坏,然后我们从1开始,遍历它不超过的所有倍数,记录下损坏的总页数。
时间复杂度为
参考代码
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long ll;
class Solution {
public:
  void solve(int case_num) {
    int n, m, q;
    cin >> n >> m >> q;
    vector<int> cnt(n + 1), torn(n + 1);
    for (int i = 0; i < m; ++i) {
      int p;
      scanf("%d", &p);
      torn[p]++;
    }
    for (int i = 1; i <= n; ++i)
      for (int j = 1; j <= n / i; ++j)
        if (torn[i * j])
          cnt[i]++;
    ll ans = 0;
    for (int i = 0; i < q; ++i) {
      int r;
      scanf("%d", &r);
      ans += n / r - cnt[r];
    }
    cout << "Case #" << case_num << ": " << ans << endl;
  }
};
int main() {
  Solution solution;
  int t;
  cin >> t;
  for (int i = 1; i <= t; ++i)
    solution.solve(i);
}
思路2 分解因数
对于每一个缺失的页码,将其分解质因数,然后搜索得到其所有的因子,给对应的cnt加上1。(这是我比赛时用的方法,比前一种方法的工作量要大上不少,也导致了第一题用时比较久,40分钟才过。)
参考代码
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long ll;
vector<int> primes{2, 3, 5, 7};
class Solution {
  void process(int p, vector<ll> &cnt) {
    unordered_map<int, int> factor;
    int i = 0;
    int p0 = p;
    while (p > 1) {
      while (p % primes[i] == 0) {
        factor[primes[i]]++;
        p /= primes[i];
      }
      i++;
      if (primes[i] * primes[i] > p) {
        if (p > 1)
          factor[p]++;
        break;
      }
    }
    vector<int> nums{1};
    for (const auto &f : factor) {
      int n = nums.size();
      for (int j = 0; j < n; ++j) {
        int m = nums[j];
        for (int k = 1; k <= f.second; ++k) {
          m *= f.first;
          nums.emplace_back(m);
        }
      }
    }
    for (int num : nums) {
      cnt[num] += 1;
    }
  }
public:
  void solve(int case_num) {
    int n, m, q;
    cin >> n >> m >> q;
    vector<ll> cnt(n + 1);
    for (int i = 0; i < m; ++i) {
      int p;
      scanf("%d", &p);
      process(p, cnt);
    }
    ll ans = 0;
    for (int i = 0; i < q; ++i) {
      int r;
      scanf("%d", &r);
      ans += n / r - cnt[r];
    }
    cout << "Case #" << case_num << ": " << ans << endl;
  }
};
int main() {
  Solution solution;
  int t;
  cin >> t;
  for (int i = 4; i < 501; ++i) {
    int n = 2 * i + 1;
    int j = 0;
    bool prime = true;
    while (primes[j] * primes[j] <= n) {
      if (n % primes[j] == 0) {
        prime = false;
        break;
      }
      j++;
    }
    if (prime)
      primes.emplace_back(n);
  }
  for (int i = 1; i <= t; ++i)
    solution.solve(i);
}
Problem B - The Equation
题目描述
有个非负整数,以及一个目标值,请找出满足的最大的非负整数。如果不存在这样的,输出-1。
数据范围:。
题解
这一题的小数据集数据规模非常小,数字上限只到100,所以暴力穷举就可以通过。但这一做法显然对于大数据集是不奏效的。
所有与异或相关的题目,必然要考虑二进制有关的性质。我们首先考虑和式,不妨看一个例子:
| 数字 | 二进制表示 | 
|---|---|
| A1=15 | 1 1 1 1 | 
| A2=8 | 1 0 0 1 | 
| A3=6 | 0 1 1 0 | 
| A4=3 | 0 0 1 1 | 
| 1的个数 | 2 2 3 3 | 
| 0的个数 | 2 2 1 1 | 
| ----- | ------- | 
| k=7 | 0 1 1 1 | 
在计算异或和的时候,我们原本是计算与每一个的异或值,然后求和。但实际上,通过观察,可以发现,异或计算的每一位是独立的,也即,我们可以计算的每一位与所有这一位的异或值,最后对所有的结果求和。
这启发我们用两个数组ones[64]和zeros[64]统计所有二进制表示中每一位上1和0的数量。然后就可以把刚才的和式重写为:
其中已经写成二进制形式。这里上限取50,是因为,所以最终的结果不会超过。
写成这样的形式之后,我们不难找到一条贪心策略。我们从二进制最高位开始,如果这一位上设为1后,可以异或和满足不超过的条件,我们就把这一位设为1,因为这样得到的数一定比把这一位设为0得到的数大。
我们如何判断能不能满足条件呢?对于第位来说,它产生的和是有最小值的,这个最小值就是。所以我们可以预先计算出从第0位到第50位的最小值,并求出累积和min_val[64]。
得到这一累积和后,我们就可以很容易地判断某一位上取1之后,后面的位置是否存在一种取法,使得总异或和不超过。只要这一条件能够满足,我们就取1。否则检查取0时能否满足,如果能满足,就取0。如果取0也不行,说明无解。
参考代码
#include <algorithm>
#include <bitset>
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
class Solution {
public:
  void solve(int case_num) {
    ll n, m;
    cin >> n >> m;
    vector<ll> a(n), ones(64), zeros(64);
    for (int i = 0; i < n; ++i) {
      ll d;
      cin >> d;
      bitset<64> bs(d);
      for (int j = 0; j < 64; ++j) {
        if (bs[j])
          ones[j]++;
        else
          zeros[j]++;
      }
    }
    vector<ll> min_val(64);
    ll last = 0;
    for (int i = 0; i <= 50; ++i) {
      ll num = (1ll << i);
      last += (ll)min(ones[i], zeros[i]) * num;
      min_val[i] = last;
    }
    ll sum = 0;
    bitset<64> k(0);
    for (int i = 50; i >= 0; --i) {
      ll num = (1ll << i);
      ll one = num * zeros[i];
      ll zero = num * ones[i];
      if (sum + one + (i > 0 ? min_val[i - 1] : 0) <= m) {
        k.set(i);
        sum += one;
      } else if (sum + zero + (i > 0 ? min_val[i - 1] : 0) <= m) {
        sum += zero;
      } else {
        cout << "Case #" << case_num << ": " << -1 << endl;
        return;
      }
    }
    ll ans = k.to_ullong();
    cout << "Case #" << case_num << ": " << ans << endl;
  }
};
int main() {
  Solution solution;
  int t;
  cin >> t;
  for (int i = 1; i <= t; ++i)
    solution.solve(i);
}
Problem C - Shifts
题目描述
有A和B两个保安,他们要排一个天的值班表。每一天至少要有一个人值班,也可以两个人都值班。A第天值班的快乐值为,B第天值班的快乐值为。求出一共有多少种排法,可以使得A和B的快乐值总和都不少于。
数据范围:
题解
这题有一个很容易看出来的穷举方法。因为要求每一天至少有一个人值班,所以每一天实际上有三种情况:A值班,B值班,或者A、B都值班。那么我们就可以穷举每一天的情况,然后检查是否满足条件。这样的时间复杂度是,对于小数据集没有问题,但大数据集,而约等于,显然会超时。
怎么办?
思路1 剪枝
剪枝是搜索类问题中的常用策略。这一题可以怎么剪枝呢?
- 如果穷举到某一天时,发现即使后面所有天都给A排班,A的快乐值也不够,或者所有天都给B排班,B的快乐值也不够,那么这条分支就不用继续向下搜索了。
 - 如果穷举到某一天时,发现已经满足A和B的快乐值都不少于H,那么剩下来的天一共种情况都能满足要求。所以直接累加到总的方法数中,不再继续搜索。
 
由于本题数据比较弱,使用这两个剪枝条件就可以通过了。
参考代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
vector<ll> three;
class Solution {
  ll ans, n, h;
  vector<ll> ca, cb;
  void dfs(vector<ll> &a, vector<ll> &b, ll asum, ll bsum, int n) {
    if (n == 0) {
      if (asum >= h && bsum >= h)
        ans++;
      return;
    }
    for (int i = 1; i <= 3; ++i) {
      ll na = asum, nb = bsum;
      if (i & 1)
        na += a[n - 1];
      if (i & 2)
        nb += b[n - 1];
      if (ca[n - 1] + na < h || cb[n - 1] + nb < h)
        continue;
      if (na >= h && nb >= h) {
        ans += three[n - 1];
        continue;
      }
      dfs(a, b, na, nb, n - 1);
    }
  }
public:
  void solve(int case_num) {
    cin >> n >> h;
    vector<ll> a(n), b(n);
    ca = vector<ll>(n + 1);
    cb = vector<ll>(n + 1);
    for (int i = 0; i < n; ++i) {
      scanf("%lld", &a[i]);
      ca[i + 1] = ca[i] + a[i];
    }
    for (int i = 0; i < n; ++i) {
      scanf("%lld", &b[i]);
      cb[i + 1] = cb[i] + b[i];
    }
    if (ca[n] < h || cb[n] < h) {
      cout << "Case #" << case_num << ": " << 0 << endl;
      return;
    }
    ans = 0;
    dfs(a, b, 0, 0, n);
    cout << "Case #" << case_num << ": " << ans << endl;
  }
};
int main() {
  Solution solution;
  ll n = 1;
  for (int i = 0; i < 21; ++i) {
    three.emplace_back(n);
    n *= 3l;
  }
  int t;
  cin >> t;
  for (int i = 1; i <= t; ++i)
    solution.solve(i);
}
思路2 状压DP
朴素穷举的最大问题是底数是3,如果我们能把底数降到2,就是一个可以处理的数据规模了。
很容易想到把A和B分开处理。计算A能够满足条件的排法,再计算B能够满足条件的排法。问题是,如何去掉不合法的情况,也即存在没有人站岗的情况的排法?
我们用一个位的二进制数state表示A每天的站岗情况,1表示A这天站岗,0表示A这天不站岗。我们先假设A、B不同时站岗。那么A不站岗的时候,B就要站岗。我们可以计算出每一个state对应的B得到的快乐值,如果不少于,就记为f[state]=1。
接下来,我们需要派生出包含A、B都站岗的情形。对于一个已经满足B的state,我们保持B的状态不变,然后将其中的0替换为1,就可以得到所有满足B的快乐值的(A,B)状态组。这样可以保证每天至少有一个人站岗。
比如说,如过state是1001,且满足B的快乐值不少于,那么就可以从1001这个state,得到了(1001, 1001)、(1101, 1001)、(1011, 1001)、(1111, 1001)这几个状态。第二个数,实际上就是B的排班表,不过0和1正好是反过来的,也即0站岗,1不站岗。
为了不重复不遗漏,在派生过程中,我们需要一位一位进行(可以参考代码中这部分内外循环的顺序)。
这样,我们就得到了所有满足B的快乐值不小于的方法,然后,我们再检查是否满足A的快乐值不小于即可。
参考代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
class Solution {
public:
  void solve(int case_num) {
    ll n, h;
    cin >> n >> h;
    ll states = (1 << n);
    vector<ll> a(n), b(n), f(states);
    for (int i = 0; i < n; ++i) {
      scanf("%lld", &a[i]);
    }
    for (int i = 0; i < n; ++i) {
      scanf("%lld", &b[i]);
    }
    // 检查B的快乐值是否被满足。
    for (int i = 0; i < states; ++i) {
      ll sum = 0;
      for (int j = 0; j < n; ++j)
        if (!(i & (1 << j)))
          sum += b[j];
      if (sum >= h)
        f[i]++;
    }
    // 派生B的状态。
    // 注意这里循环的顺序,要把位的循环放在外层,才能保证不重复不遗漏。
    for (int j = 0; j < n; ++j)
      for (int i = 0; i < states; ++i)
        if (i & (1 << j))
          f[i] += f[i ^ (1 << j)];
    // 检查A的快乐值是否被满足。
    ll ans = 0;
    for (int i = 0; i < states; ++i) {
      ll sum = 0;
      for (int j = 0; j < n; ++j)
        if (i & (1 << j))
          sum += a[j];
      if (sum >= h)
        ans += f[i];
    }
    cout << "Case #" << case_num << ": " << ans << endl;
  }
};
int main() {
  Solution solution;
  int t;
  cin >> t;
  for (int i = 1; i <= t; ++i)
    solution.solve(i);
}