【比赛】USACO22FEB 题解(铜、银组)
青铜组
过了。
A - Sleeping in Class
Des
给 \(n\) 个数 \(a_i\),可以随便选择两个相邻的数改成一个数,为这个这两个数的和。问最后使得每个数相等的最小操作次数。
保证 \(n\le 10^5\),\(\sum a_i\le 10^6\).
Sol
\(\sum a_i\) 这么小着实有点离谱。考虑最后每一坨的和一定是所有数的和的约数,枚举这个约数然后 \(O(n)\) check 是否可行。约数最多 \(2^7\) 个。
My Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int T, n, a[N];
bool check(int x) {
int res = 0;
for(int i = 1; i <= n; i++) {
if(res + a[i] <= x) res += a[i];
else if(res != x) return false;
else res = a[i];
}
return true;
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
for(cin >> T; T; T--) {
cin >> n;
int sum = 0;
for(int i = 1; i <= n; i++)
cin >> a[i], sum += a[i];
for(int i = 0; i <= sum; i++) {
if((i == 0 || sum % i == 0) && check(i)) {
cout << (i == 0 ? 0 : n - sum / i) << '\n';
break;
}
}
}
return 0;
}
B - Photoshoot 2
Des
给定 \(n\) 个数 \(a_i\),再给 \(n\) 个数 \(b_i\). 保证 \(\{a\}\) 和 \(\{b\}\) 都是 \(1\sim n\) 的一个排列。
每次操作可以把 \(\{a\}\) 中的某个数移动到数组中的任意位置,求变成 \(\{b\}\) 的最小次数。
Sol
有一个经典问题是问只能交换相邻的两个数,然后把 \(\{a\}\) 变到 \(\{b\}\) 的最小次数。由于交换一次最多减少一个逆序对,而且如果存在逆序对就不可能没有相邻的逆序对(随便证一下就知道),所以答案就是「相对位置」的逆序对个数。
然后到这题就是换成每次可以移动一个数到任意位置,等价于多做几次交换相邻的数的操作。
那么对于 \(\{b\}\) 中的数,如果一个数后面没有和它构成逆序对的数逆序对,那么不需要操作它。否则只需要操作一次就可以使得它不和后面的数构成逆序对。用树状数组维护一下就好了。
My Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, a[N], b[N], p[N];
struct BIT {
int v[N];
void add(int x, int k) {
while(x <= n) {
v[x] += k;
x += x & -x;
}
}
int query(int x) {
int ans = 0;
while(x) {
ans += v[x];
x -= x & -x;
}
return ans;
}
} c;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i], p[a[i]] = i;
for(int i = 1; i <= n; i++) cin >> b[i];
int cnt = 0;
for(int i = n; i >= 1; i--) c.add(p[b[i]], 1);
for(int i = 1; i <= n; i++) {
c.add(p[b[i]], -1);
if(c.query(p[b[i]])) ++cnt;
}
cout << cnt << '\n';
return 0;
}
C - Blocks
过水已隐藏。
#include <bits/stdc++.h>
using namespace std;
int n, len;
bool has[5][26], use[5];
string s[5];
char t[10];
bool ok;
void dfs(int d) {
if(d == len) {
ok = true;
return;
}
for(int i = 1; i <= 4; i++)
if(!use[i] && has[i][t[d + 1] - 'A']) {
use[i] = true;
dfs(d + 1);
use[i] = false;
}
}
int main() {
cin >> n;
for(int i = 1; i <= 4; i++) {
cin >> s[i];
for(auto c : s[i]) has[i][c - 'A'] = true;
}
for(int i = 1; i <= n; i++) {
scanf("%s", t + 1);
ok = false, len = strlen(t + 1);
dfs(0);
if(ok) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}
银组
B 题 AC 了,A 题最后 5 分钟 rush 了暴力。但是只拿到一个点。。。
寄了。。。
A - Redistributing Gifts
Des
有 \(n\) 个礼物分给 \(n\) 头奶牛。最开始是把第 \(i\) 号礼物分给了第 \(i\) 号奶牛。
每头奶牛有一个偏好列表。表示了想要的礼物的排名。可以任意重排礼物的分配方式,使得每头奶牛拿到的礼物在该奶牛的偏好列表里的排名不低于最开始拿到的礼物的排名。
问每头奶牛可能拿到的排名最高的礼物是什么。
\(n\le 500\)。
Sol
考虑对于每头奶牛 BFS 拓展能拿到的礼物。每次用队首的礼物去尝试交换其他牛的,如果可以交换并且没有拿到过那头牛的礼物,就存进队列里。
交换给对方的礼物必须满足对方的要求(不低于初始的排名),但自己拿到的礼物可以暂时不满足要求。
可以证明一种合法的安排礼物的方式必然能由上面的过程得到,所以这个做法是对的。
时间复杂度 \(O(n^3)\)。
My Code
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int n, a[N][N], rk[N][N];
bitset<N> ok[N];
queue<int> q;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
cin >> a[i][j], rk[i][a[i][j]] = j;
for(int i = 1; i <= n; i++) {
ok[i][i] = true, q.push(i);
while(!q.empty()) {
int u = q.front();
q.pop();
for(int j = 1; j <= n; j++) {
if(!ok[i][j] && rk[j][u] <= rk[j][j]) {
ok[i][j] = true;
q.push(j);
}
}
}
int ans = n + 1;
for(int j = 1; j <= n; j++)
if(ok[i][j] && rk[i][j] < ans) ans = rk[i][j];
cout << a[i][ans] << '\n';
}
return 0;
}
B - Robot Instructions
Des
需要在平面直角坐标系上从 \((0,0)\) 走到 \((x_g,y_g)\)。有 \(n\) 条指令 \((x_i,y_i)\) 表示先向右走 \(x_i\) 步,再向上走 \(y_i\) 步。对于每个 \(k=1\sim n\) 求出依次执行 \(k\) 条从 \(n\) 条指令里选出的指令后到达 \((x_g,y_g)\) 的方案数(这些指令得按原来的顺序依次执行)。
\(n\le 40\),坐标在 \(10^9\) 范围内。
Sol
meet in the middle 即可。DFS 预处理后二十步能走到的状态(点 + 步数)。然后枚举前二十步选择的状态,再枚举步数,统计答案。复杂度 \(O(2^nn)\)。要不是经 Josh 提醒,我一直会疑惑为什么我 \(O(2^nn^2)\) 的代码怎么过不了(当然过不了)。
然后不能用 map
存状态,会 T。。。乱哈希一下用 unordered_map
勉强卡过。
My Code
#include <bits/stdc++.h>
#define mt make_tuple
#define fi first
#define se second
#define eb emplace_back
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
const int N = 45;
int n, sc, dx[N], dy[N], xg, yg, cnt[1 << 21][22];
ll ans[N];
pair<ll, ll> sta[1 << 21];
inline ll mp(ll x, ll y) {
return (x % 998244353) * 1e9 + (y % (ll)(1e9 + 7));
}
unordered_map<ll, int> s;
void dfs(int p, int c, ll x, ll y, bool lst) {
if(lst) {
if(s.find(mp(x, y)) == s.end()) s[mp(x, y)] = ++sc, sta[sc] = make_pair(x, y);
++cnt[s[mp(x, y)]][c];
}
if(p == 21) return;
dfs(p - 1, c, x, y, false);
dfs(p - 1, c + 1, x - dx[p - 1], y - dy[p - 1], true);
}
int main() {
cin >> n >> xg >> yg;
for(int i = 1; i <= n; i++) cin >> dx[i] >> dy[i];
if(n <= 20) {
for(int i = 0; i < (1 << n); i++) {
ll x = 0, y = 0;
int pc = 0;
for(int j = 0; j < n; j++)
if(i >> j & 1) x += dx[j + 1], y += dy[j + 1], ++pc;
if(x == xg && y == yg) ++ans[pc];
}
} else {
dfs(n + 1, 0, xg, yg, true);
for(int i = 0; i < (1 << 20); i++) {
ll x = 0, y = 0;
int pc = 0;
for(int j = 0; j < 20; j++)
if(i >> j & 1) x += dx[j + 1], y += dy[j + 1], ++pc;
ll to = s[mp(x, y)];
for(int j = 0; j + pc <= n && j <= n - 20; j++)
ans[j + pc] += cnt[to][j];
}
}
for(int i = 1; i <= n; i++) cout << ans[i] << '\n';
return 0;
}
C - Email Filing
Des
\(M\) 个文件夹,\(N\) 封邮件展示在两个窗口,每个窗口都最多展示 \(K\) 个邮件或文件夹。每个邮件有一个属性 \(f_i\) 表示它应该放到哪个文件夹里。已经被放好的邮件会消失,同时窗口下面(或上面)如果还有邮件,就会补上。FJ 需要把所有的邮件都放好,但他的鼠标滚轮坏了,只能往下滑。
问 FJ 是否能放好所有邮件。
\(M\le 10^5, N\le 10^6\)。
Sol
主要的思路是用优先队列(小根堆)把右边窗口的邮件存起来,然后每次看所在文件夹编号最小的文件是否能放进左边最小的文件夹。如果不能放,就把右边窗口往下滑,如果能放,就模拟邮件补位的过程(从上面或从下面补)。由于文件夹被划过了就没了,所以当且仅当一个文件夹的所有邮件都被放好才能把文件夹往下滑。
标记一下右边窗口里邮件的左端点 \(fl\) 和右端点 \(fr\),模拟补位的过程是把 \(fr\) 加一或者把 \(fl\) 减一(取决于邮件窗口是否滑到底了)。把邮件窗口往下滑的过程是把 \(fl\) 和 \(fr\) 同时加一,并把左边跳过的邮件存进一个 vector 里,等到邮件窗口滑到底后补位。你发现区间 \(fl\) 位置对应的邮件可能已经被放好了,只需要给放好的邮件打好标记,然后保证 \(fl\) 的位置是没有放好的邮件就行了。
My Code
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
#define mp make_pair
#define fi first
#define se second
const int N = 1e5 + 5;
int T, m, n, k, cnt[N], a[N], vis[N];
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
for(cin >> T; T; T--) {
cin >> m >> n >> k;
for(int i = 1; i <= n; i++) cnt[i] = 0, vis[i] = 0;
deque<int> l; // 存储被跳过的邮件
priority_queue<pii, vector<pii>, greater<pii>> q;
for(int i = 1; i <= n; i++) {
cin >> a[i];
if(i <= k) q.push(mp(a[i], i));
++cnt[a[i]];
}
int fl = 1, fr = k, fold = 1;
while(q.size() || l.size()) {
while(!cnt[fold] && fold < m - k + 1) ++fold;
if(q.size() && q.top().fi >= fold && q.top().fi <= fold + k - 1) {
--cnt[q.top().fi];
vis[q.top().se] = true; // 记录已经放好的邮件
q.pop();
if(fr < n) {
++fr;
q.push(mp(a[fr], fr));
}
} else if(fr < n) { // 邮件窗口没有滑到底时模拟补位
++fl, ++fr;
q.push(mp(a[fr], fr));
l.push_back(a[fl - 1]); // 记录跳过的邮件
} else if((int)q.size() < k && l.size()) { // 邮件窗口滑到底之后模拟补位
--fl;
q.push(mp(l.back(), fl));
l.pop_back();
} else break;
while(q.size() && q.top().se < fl) q.pop(); // 保证堆顶的邮件是没有被跳过的
while(vis[fl]) ++fl; // 保证 fl 位置的邮件是没有放好的
}
if(q.size()) cout << "NO\n";
else cout << "YES\n";
}
return 0;
}