ARC179 补
A partition
给定一个长度为 \(n\) 的序列 \(x\),求将其重排后满足条件的重排方案,或报告无解。
条件:构造一个序列 \(y\),\(y_0=0,y_i=\sum\limits_{j = 1}^{i}x_j\),满足小于 \(k\) 的数全部在大于等于 \(k\) 的数右侧。
Solution
-
当 \(k > 0\) 时,\(k\) 左侧为 \(0\),所以 \(0\) 不会对答案产生影响,直接排序即可。因为单调所以满足条件。
-
当 \(k \leq 0\) 时,\(k\) 右侧为 \(0\),所以右边的所有数必须比 \(0\) 大。可以倒序排序,这样可以让前缀和都尽量大,如果还不够条件就无解了。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1;
int n, k, a[kN];
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a + 1, a + n + 1);
if (k <= 0) {
reverse(a + 1, a + n + 1);
if (accumulate(a + 1, a + n + 1, 0ll) < k) {
cout << "No\n";
return 0;
}
}
cout << "Yes\n";
for (int i = 1; i <= n; i++) {
cout << a[i] << ' ';
}
return 0;
}
B Between B and B
给定一个长度为 \(m(m\leq 10)\) 的序列 \(x\),求满足条件的长度为 \(n\) 的序列 \(a\) 个数。
条件:\(a\) 值域 \([1,m]\),且若 \(a_l=a_r=k\),则 \(\exist b\in[l,r],b = x_k\)。
Solution
因为 \(m\) 极小,可以考虑状压每个 \(k=a_i\) 有没有出现后面的 \(x_k\)。
即令 \(f_{i, s}\) 表示当前考虑到 \(i\),\(s\) 的第 \(j\) 位表示 \(j\) 后面有没有出现 \(x_j\)。
随便转移。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <deque>
#include <iostream>
#include <map>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e3 + 1;
int n;
map<PII, int> M;
bool cmp(int i, int j) {
if (M.count({i, j})) {
return M[{i, j}];
}
bool ans;
cout << "? " << i << ' ' << j << endl;
cin >> ans;
return M[{i, j}] = ans;
}
deque<int> q;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++) {
q.push_back(i);
}
sort(q.begin(), q.end(), cmp);
for (int i = n; i >= 2; i--) {
cout << "+ " << q.front() << ' ' << q.back() << endl;
q.pop_front(), q.pop_back();
int id;
cin >> id;
q.insert(lower_bound(q.begin(), q.end(), id, cmp), id);
}
cout << "!" << endl;
return 0;
}
C Beware of Overflow
给定一个整数 \(n\leq 1000\),在交互库内有一个整数 \(r\) 和一个长度为 \(n\) 的序列 \(a\),保证 \(a_i<r,\sum a_i<r\)。请通过不超过 \(25000\) 次操作将序列 \(a\) 合并为同一个数。
- 操作 1:
+ i j
表示将 \(a_i\) 和 \(a_j\) 删除并将其和插入序列末端,返回其下标。 - 操作 2:
? i j
表示询问 \(a_i\) 和 \(a_j\) 大小关系,返回 \([a_i<a_j]\)。
Solution
可以利用操作 2 做 cmp,直接 sort,每次把最大值和最小值相加,直接把新数插入序列中,位置直接 lower_bound。显然是 \(2n\log n\) 次操作的。
这个题目让我知道了 sort 和 lower_bound 对 cmp 调用次数是不超过 \(n\log n\) or \(\log n\) 的。这很帅欸。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <deque>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e3 + 1;
int n;
bool cmp(int i, int j) {
bool ans;
cout << "? " << i << ' ' << j << endl;
cin >> ans;
return ans;
}
deque<int> q;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++) {
q.push_back(i);
}
sort(q.begin(), q.end(), cmp);
for (int i = n; i >= 2; i--) {
cout << "+ " << q.front() << ' ' << q.back() << endl;
q.pop_front(), q.pop_back();
int id;
cin >> id;
q.insert(lower_bound(q.begin(), q.end(), id, cmp), id);
}
cout << "!" << endl;
return 0;
}
D Portable Gate
给定一棵大小为 \(n\) 的树,起始点终止点自定,问最少付出多少代价可以遍历完整棵树。
操作:放置传送门或回到传送门,代价为 \(0\);走一条边,代价为 \(1\)。
Solution
显然按 dfs 序遍历代价最小。无确定起始点,没有根,可以自定,然后换根 dp。
不难猜到一个错误的结论:每次遇到分叉就放传送门。反例:一条链挂一个分叉,最小代价方案为在链顶放传送门。
走法的结论实在是没有了,不如直接考虑树形 dp。
设 \(f_{i, 0/1}\) 表示遍历完以 \(i\) 为根的子树,不需要 or 必须回到 \(i\) 所付出的最小代价。
\(f_{i, 1}\) 有两种方案,一种是从 \(i\) 的儿子转移过来,父子边直接走。另一种方案是在 \(i\) 这里放传送门,走遍整个子树传送回来,就是 \(2siz_i-dep_i\)。
\(f_{i, 0}\) 类似的,选择一个儿子不出来,要出来的儿子处理方法和 \(f_{i, 1}\) 基本相同。不出来可以直接走父子边转移。
换根即可。
没写程序,这么恶心的换根谁爱写谁写。
E Rectangle Concatenation
给定 \(n\) 个矩形,问其中有几个区间满足条件。
条件:按原序列顺序拼合矩形,可以在拼合的过程中一直保持拼出来的图形是矩形。
Solution
考虑 \(O(n^2)\) 做法是怎么做的。
可以枚举区间左端点,在右端点右移的时候,即添加新的矩形,暴力模拟即可。
模拟是绝对没办法优化的,考虑表示成一个 \(O(n^2)\) 的 dp。
枚举左端点 \(l\),令 \(f_{r, 0/1}\) 表示第 \(r\) 个矩形在拼合时是 x/y 相同而拼是否可能。
考虑 \(f_{r, 0}\),\(f_{r,1}\) 旋转而完全相同,无需特别考虑。从 \(f_{r-1, 0}\) 和 \(f_{r-1, 1}\) 转移过来。
若从 \(f_{r-1,0}\) 转移,则整体矩形的 \(x=x_{r-1}\)。所以只要 \(x_r=x_{r-1}\) 即可转移过来。
\(f_{r-1,1}\) 来转移的话,整体矩形的 \(x\) 不好直接算,记录上次出现 \(0\) 加区间和也是没有前途。但是可以计算出区间矩形的面积之和去除以 \(y_{r-1}\),因为整体矩形的 \(y=y_{r-1}\)。
思考一下,这个 dp 转移,它在哪里浪费了时间?
你发现了,枚举 \(l\) 动 \(r\) 只能一个一个加矩形,这样加入矩形次数达到了 \(O(n^2)\)。
但是如果我换一下,枚举 \(r\) 动 \(l\) 呢?这样我的区间右端点固定,每次加矩形总是加一整个序列的,我加矩形的次数仅仅 \(O(n)\)。
可是怎么转移呢?你突然发现,\(O(n^2)\) 做法的第一种转移方式是 \(f_{r-1, 0}\) 和 \([x_r=x_{r-1}]\) 取并,这个转移方式就导致每次改变右端点,要么继承上一次的答案,要么直接删除,以后再也不可能这么拼。第二种转移方式是拿面积和去除以 \(y_{r-1}\) 要等于 \(x_r\),但是我现在知道 \(y_{r-1}\) 也知道 \(x_r\),是不是面积和是一定的?那面积和一定,是不是对应着产生改变的 \(y\) 也是一定的,只有一个。
均摊下来 \(O(n)\)。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <unordered_map>
#include <vector>
using namespace std;
using LL = long long;
using Rec = pair<int, int>;
constexpr int kN = 3e5 + 1;
#define x first
#define y second
int n;
Rec a[kN];
LL s[kN], ans, cur;
unordered_map<LL, int> M;
bool t[2][kN];
vector<int> v[2];
void R(int tp, int x) {
cur -= (t[tp][x] == 1 && t[!tp][x] == 0);
t[tp][x] = 0, v[tp].pop_back();
}
void A(int tp, int x) {
cur += (t[tp][x] == 0 && t[!tp][x] == 0);
t[tp][x] = 1, v[tp].push_back(x);
}
int main() {
freopen("hack.txt", "r", stdin);
cin.tie(0)->sync_with_stdio(0);
cin >> n, M[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i].x >> a[i].y;
s[i] = s[i - 1] + 1ll * a[i].x * a[i].y;
M[s[i]] = i;
}
auto F = [](LL x) { return M.count(x) ? M[x] + 1 : -1; };
for (int i = 1; i <= n; i++) {
int px = i >= 2 ? F(s[i - 1] - 1ll * a[i].x * a[i - 1].y) : -1;
int py = i >= 2 ? F(s[i - 1] - 1ll * a[i].y * a[i - 1].x) : -1;
bool tx = px == -1 ? 0 : t[1][px];
bool ty = py == -1 ? 0 : t[0][py];
if (a[i].x != a[i - 1].x) {
for (; !v[0].empty(); R(0, v[0].back())) {
}
}
if (a[i].y != a[i - 1].y) {
for (; !v[1].empty(); R(1, v[1].back())) {
}
}
tx && !t[0][px] && (A(0, px), 0);
ty && !t[1][py] && (A(1, py), 0);
A(0, i), A(1, i);
ans += cur;
}
cout << ans << '\n';
return 0;
}