解题思路
使用队列结构来模拟取钱队列的方式,但是Test Set 2超时了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <iostream> #include <queue>
using namespace std;
void cal(int T) { int N, X, A; cin >> N >> X;
queue<pair<int, int>> Q; for (int i = 1; i <= N; ++i) { cin >> A; Q.push({A, i}); }
cout << "Case #" << T << ":";
while (!Q.empty()) { auto p = Q.front(); Q.pop(); if (p.first <= X) { cout << " " << p.second; } else { p.first -= X; Q.push(p); } } cout << endl; }
int main() { int T; cin >> T;
for (int i = 1; i <= T; ++i) { cal(i); }
return 0; }
|
优化
观察可以得知,取相同次数就能够完成需求的所有人,他们的相对位置与初始一致。因此可以直接计算某人需要取多少次钱,然后进行排序即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <iostream> #include <vector> #include <algorithm>
#define UPDIV(A, X) (((A) + (X) - 1) / (X))
using namespace std;
void cal(int T) { int N, X, A; cin >> N >> X;
vector<pair<int, int>> Q; for (int i = 1; i <= N; ++i) { cin >> A; Q.emplace_back(UPDIV(A, X), i); } sort(Q.begin(), Q.end(), [](const pair<int, int>& A, const pair<int, int>& B) -> bool { if (A.first != B.first) { return A.first < B.first; } return A.second < B.second; });
cout << "Case #" << T << ":";
for (const auto &q : Q) { cout << " " << q.second; }
cout << endl; }
int main() { int T; cin >> T;
for (int i = 1; i <= T; ++i) { cal(i); }
return 0; }
|
解题思路
因为已知所有时间间隔不会重叠,因此直接对所有输入的时间间隔的$S_i$进行排序。
然后采用贪心法,从每个间隔的起点开始,使用二分查找搜寻第一个大于机器人工作结束时间点的时间间隔(即,$E_j > S_i + K\text{, 且}j > i$);找到之后再次比较$S_j$和$S_i + K$的大小。如果$S_j > S_i + K$,那么说明该机器人最多可以工作到第$j - 1$个时间间隔;反之说明当前间隔$j$还需要一个机器人来处理。这样就可以知道当前机器人,最终能经过多少个时间间隔。
这样也超时了吗。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #include <iostream> #include <vector> #include <algorithm>
typedef long long ll;
using namespace std;
int binSearch(vector<pair<ll, ll>>& times, int start, int end, ll target) { int L = start; int R = end; int mid;
while (L <= R) { mid = L + ((R - L) >> 1);
if (times[mid].second <= target) { L = mid + 1; } else if (mid == start || times[mid - 1].second <= target) { return mid; } else { R = mid - 1; } }
return -1; }
ll cal (int N, int K) { vector<pair<ll, ll>> times(N);
for (int i = 0; i < N; ++i) { cin >> times[i].first >> times[i].second; }
sort(times.begin(), times.end(), [](const pair<ll, ll>& A, const pair<ll, ll>& B) -> bool { return A.first < B.first; });
ll robot_end = 0; ll count = 0;
int time_pos = 0;
while (time_pos < N) { robot_end = times[time_pos].first + K; int pos = binSearch(times, time_pos, N - 1, robot_end);
++count; if (pos == -1) { break; } time_pos = pos;
if (robot_end > times[time_pos].first) { times[time_pos].first = robot_end; } }
return count; }
int main() { int T, N, K; cin >> T;
for (int i = 1; i <= T; ++i) { cin >> N >> K; cout << "Case #" << i << ": " << cal(N, K) << endl; }
return 0; }
|
优化
经过分析发现,上述方法忽略了一种极端情况。即当一个时间间隔非常大的时候,在该间隔内可能需要很多的机器人,但是如果对其中每一个机器人都调用二分查找的话,则会浪费大量的时间。
新增了一个计算$num = UPDIV(E_i - S_i, K)$,来计算出当前间隔需要多少个机器人。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| #include <iostream> #include <vector> #include <algorithm>
#define UPDIV(A, X) (((A) + (X) - 1) / (X))
typedef long long ll;
using namespace std;
int binSearch(vector<pair<ll, ll>>& times, int start, int end, ll target) { int L = start; int R = end; int mid;
while (L <= R) { mid = L + ((R - L) >> 1);
if (times[mid].second <= target) { L = mid + 1; } else if (mid == start || times[mid - 1].second <= target) { return mid; } else { R = mid - 1; } }
return -1; }
ll cal (int N, int K) { vector<pair<ll, ll>> times(N);
for (int i = 0; i < N; ++i) { cin >> times[i].first >> times[i].second; }
sort(times.begin(), times.end(), [](const pair<ll, ll>& A, const pair<ll, ll>& B) -> bool { return A.first < B.first; });
ll robot_end = 0; ll count = 0;
int time_pos = 0;
while (time_pos < N) {
ll num = UPDIV(times[time_pos].second - times[time_pos].first, K);
robot_end = times[time_pos].first + num * K; int pos = binSearch(times, time_pos, N - 1, robot_end);
count += num; if (pos == -1) { break; }
time_pos = pos;
if (robot_end > times[time_pos].first) { times[time_pos].first = robot_end; } }
return count; }
int main() { int T, N, K; cin >> T;
for (int i = 1; i <= T; ++i) { cin >> N >> K; cout << "Case #" << i << ": " << cal(N, K) << endl; }
return 0; }
|
解题思路
博弈类型的题实在是不熟悉,因此主要思路参考了文章:[Google Kickstart 2020][校招笔试][Round F]全部题目+题解
整体方法来说就是暴力穷举,通过递归的方式,让A和B做一次最佳的决策,然后再进一步分析后续的可能。如果当前最佳策略存在多种,那么就遍历所有可能,寻找出最佳的结果。因此关键在于什么样的策略是最佳的决策。
对于这道题,A的最优策略就是走尽可能多的房间,B的最优策略就是让A走尽可能少的房间。因此当A走出一步的时候,B要找出当前所有可能的下一步中得分最少的选择;而A则需要在所有当前所有可能的下一步中,找出得分最多的选择(在B的阻碍之下)。
所以题目就抽象成了:
- 遍历A当前所有的下一步 $ Next_A_i $
- 然后进一步遍历B基于 $ Next_A_i $ 的所有下一步 $ Next_B_i $
- 从所有的 $ Next_B_i $ 中,找出使得得分最少的那一个 $ Next_B $
- 因此A当前所有的下一步均有一个最少得分 $ MIN_i $ ,A再从中选出得分最高的下一步。
这里出题人给的样例范围确实很巧妙,暴力法竟然没有超时。但是优化的话,很容易联想到剪枝和记忆化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
| #include <iostream> #include <vector> #include <algorithm>
#define BLOCK 0 #define FREE 1 #define MAXDIRS 4
typedef long long ll;
using namespace std;
class Solution { public: Solution() { cin >> S >> RA >> PA >> RB >> PB >> C;
free.resize(S * S, FREE);
for (int i = 0; i < C; ++i) { int x, y; cin >> x >> y;
free[transform(x, y)] = BLOCK; }
free[transform(RA, PA)] = BLOCK; free[transform(RB, PB)] = BLOCK;
}
int solve() { return trace(RA, PA, RB, PB); }
private: int trace(int RA_, int PA_, int RB_, int PB_) { int maxPossible = 1 << 31; bool FLAGA = false;
for (int dirA = 0; dirA < MAXDIRS; ++dirA) { int Next_RA = RA_; int Next_PA = PA_;
if (canGo(dirA, Next_RA, Next_PA)) { FLAGA = true;
free[transform(Next_RA, Next_PA)] = BLOCK; int minPossible = 0x3f3f3f3f; bool FLAGB = false;
for (int dirB = 0; dirB < MAXDIRS; ++dirB) { int Next_RB = RB_; int Next_PB = PB_;
if (canGo(dirB, Next_RB, Next_PB)) { FLAGB = true;
free[transform(Next_RB, Next_PB)] = BLOCK; minPossible = min(minPossible, trace(Next_RA, Next_PA, Next_RB, Next_PB)); free[transform(Next_RB, Next_PB)] = FREE; } }
if (!FLAGB) { minPossible = 1 + trace(Next_RA, Next_PA, RB_, PB_); }
free[transform(Next_RA, Next_PA)] = FREE; maxPossible = max(maxPossible, minPossible); } }
if (!FLAGA) { int minPossible = 0x3f3f3f3f; bool FLAGB = false;
for (int dirB = 0; dirB < MAXDIRS; ++dirB) { int Next_RB = RB_; int Next_PB = PB_;
if (canGo(dirB, Next_RB, Next_PB)) { FLAGB = true;
free[transform(Next_RB, Next_PB)] = BLOCK; minPossible = min(minPossible, -1 + trace(RA_, PA_, Next_RB, Next_PB)); free[transform(Next_RB, Next_PB)] = FREE; } }
maxPossible = minPossible;
if (!FLAGB) { return 0; } }
return maxPossible; }
private: int S, RA, PA, RB, PB, C;
vector<int> free;
int transform(int x, int y) { return (x - 1) * (x - 1) + y - 1; }
bool canGo(int dir, int& X, int& Y) { int CP_X = X; int CP_Y = Y;
switch (dir) { case 0: if (CP_Y % 2 || CP_X == 1) { return false; } else { --CP_X; --CP_Y; } break; case 1: if (CP_X == S || CP_Y % 2 != 1) { return false; } else { ++CP_X; ++CP_Y; } break; case 2: if (CP_Y == 1) { return false; } else { --CP_Y; } break; case 3: if (CP_Y == CP_X * 2 - 1) { return false; } else { ++CP_Y; } break; default: return false; }
if (free[transform(CP_X, CP_Y)] == BLOCK) { return false; }
X = CP_X; Y = CP_Y; return true; } };
int main() { int T; cin >> T;
for (int i = 1; i <= T; ++i) { Solution A; cout << "Case #" << i << ": " << A.solve() << endl; }
return 0; }
|
分析
这题让我回忆起了,学习概率论的痛苦回忆。。。
先对Sample case#1进行分析,$N=3, M = 6, K = 2$,要分成两组,一组数量是1,另一组数量是2。
Pommel投了第一个骰子
Pommel投了第二个骰子
如果前两次结果相同,且都为$x$,那么第三次的结果一定要与$x$不同。
由现有条件可以得知,每次投骰子是独立的,并且结果是满足二项分布的(即,点数与$x$相同,和与$x$不同两种情况)。
这里解释一下,每个概率$p$是如何计算的。对于每个确定的投掷次数$X_i$,其概率满足N重伯努利分布,即投掷$X_i$次,恰好最后一次成功的概率。
$$
P(X = X_i) = \binom{X_i - 1}{0}p^0(1 - p)^{X_i - 1} \cdot p
$$
因此当前投出合法点数的数学期望为:
$$
E(X) = \lim_{n \to \infty} \left (\sum_{i=0}^{n} p \left (1 - p \right )^{n-1} \right ) = \frac{1}{p}
$$
投掷次数$X_i$ |
1 |
2 |
3 |
… |
n |
概率$P(X = X_i)$ |
$\frac{5}{6}$ |
$\frac{5}{6} \times \frac{1}{6}$ |
$\frac{5}{6} \times (\frac{1}{6})^2$ |
… |
$\frac{5}{6} \times (\frac{1}{6})^{n-1}$ |
代入计算可得$E(x) = 1.2$
如果前两次结果不同,那么第三次的结果必然是前两次结果中的一个,此时的$p=2/6$,同样代入计算可得$E(x) = 3$。
因此总的期望为$1 + 1 + 1.2 \times \frac{1}{6} + 3 \times \frac{5}{6} = 4.7$,其中$\frac{1}{6}$为前两次结果相同的概率,$\frac{5}{6}$为前两次结果不同的概率。
所以对于分组数大于2的情况,每一个骰子的结果都可以分成“合法”和“非法”两种结果,也就意味着其符合二项分布。
这道题的难点在于如何表示状态。因为经过上述的分析,可以发现,我们不关心某个分组的数字具体是几,仅关心新骰子的数字与某个已有分组一致,还是不同。因此可以设$k_i$表示第$i$个分组的骰子数。假设$K=3$,第一次一定是$[1, 0, 0]$,第二次的结果可以是$[2, 0,0]$也可以是$[1, 1, 0]$(前者代表两次的结果相同,后者代表第二次与第一次的结果不同)。
同时我们也不关心,分组之间的顺序。如$[1, 2, 2]$与$[2, 1, 2]$是等价的。
因此我们可以用一个排好序的分组数量来唯一的标识一个状态。
假设$M = 4, K = 2$:
如骰子序列$[1, 2, 2]$,其状态为$[0, 0, 1, 2]$。如果新增一个骰子1,那么状态会变为$[0, 0, 2, 2]$,合法。
如骰子序列$[1, 1, 2]$,其状态为$[0, 0, 1, 2]$。如果新增一个骰子2,那么状态会变为$[0, 0, 2, 2]$,合法。
而骰子序列$[1,1,3]$,其状态为$[0, 0, 1, 2]$。如果新增一个骰子2,那么状态会变为$[0, 1, 1, 2]$,不合法。
解题思路
经过上述的分析,其实很容易联想到回溯和动态规划。
整体思路参考了“Lee215”的文章https://mp.weixin.qq.com/s/Mvpe3p0GQ8N0Os4o8Zs2mQ。
这是一个迭代期望的问题,当前状态$S$进入下一状态$S’$,存在一个“投到特定点数的骰子次数的数学期望”,还有一个“所有可能的下一状态$S_{i}^{‘}$距离胜利的期望次数的均值”,两者的和即是当前状态距离胜利次数的数学期望。
之所以要对“所有可能的下一状态距离胜利的期望次数”取均值,这是因为进入每一个下一状态$S_{i}^{‘}$都有各自的概率$p_i$,将其概率$p_i$与距离胜利的期望次数相乘再求和,即是“所有可能的下一状态距离胜利的期望次数的数学期望”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| #include <iostream> #include <vector> #include <map>
typedef long long ll;
using namespace std;
class Solution { public: Solution() { cin >> N >> M >> K;
A.resize(M, 0); status.resize(M, 0);
for (int i = M - K; i < M; ++i) { cin >> A[i]; }
StatuMemo[A] = 0.0; }
double dfs() { if (StatuMemo.count(status)) { return StatuMemo[status]; }
int valid = 0; int i = 0; int j = 0; double res = 0.0;
while (i < M) { j = i;
while (j + 1 < M && status[j + 1] == status[i]) { ++j; }
if (status[j] + 1 <= A[j]) { ++status[j]; valid += j - i + 1;
res += dfs() * (j - i + 1);
--status[j]; }
i = j + 1; }
double E = M * 1.0 / valid; res = res * 1.0 / valid + E;
StatuMemo[status] = res;
return res; }
private:
int N, M, K;
vector<int> A;
vector<int> status; map<vector<int>, double> StatuMemo;
};
int main() { int T; cin >> T;
cout.precision(7); cout.setf(std::ostream::fixed);
for (int i = 1; i <= T; ++i) { Solution A; cout << "Case #" << i << ": " << A.dfs() << endl; }
return 0; }
|