Ozon Tech Challenge 2020
考试还有十分钟时放弃了QAQ, 就会前五道题, 下面来简单写一下思路
A.Kuroni and the Gifts
两个长度为n的序列(其中每个序列中元素互不相等), 上下对应相加, 求一种方案使每个上下和均不相等
两个序列分别排序上下相加即可, 后一项一定大于前一项
B.Kuroni and Simple Strings
括号序列S, 每次可以删去(), (()), ((()))这样的序列(位置不一定连续), 直到不能删为止, 问最少删几次
贪心, 每次操作从左边找一个左括号, 然后右边找个右括号, 然后左边再找, 右边再找, 直到指针移不动为止, 一次操作结束, 可以证明这样是最优的, 因为一对括号越早删除越好, 越分散两边越好
C.Kuroni and Impossible Calculation
给个序列a, 求 $\prod_{1\le i < j \le n}|a_i-a_j|$ mod P($P \le 1000, n \le 2*1e5$)
sb题, 以为有什么神奇方法
考虑暴力, 先sort一遍, 因为m很小所以搞个桶, 从前向后扫, 记录modm意义下有多少个数, 直接枚举做差就行
但n太大了, 以为有什么好的奇妙思想, 结果发现n > m的时候答案一定为零, 因为一定会有两个数modm同余QAQ
D.Kuroni and the Celebration
人生第一次做交互题, 感觉好有趣啊
就是给你一棵树, 但你不知道它的根, 你可以询问不多于$\frac n2$次x点和y点的lca是谁, 最后说出来它的根
其实挺简单的, 两个点的lca一定在两个点的路径上, 你每次都查询两个叶子节点的lca, 那么lca到x, y两个点所在的子树都不可能是根, 如果可能的话, 就是从x向上走又往回倒了, 这肯定是不行的, 每次至少会减少两个叶子, 或者直接判定一个叶子是root, 可以看代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
template <typename T>
void read(T &x) {
x = 0; bool f = 0;
char c = getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=1;
for (;isdigit(c);c=getchar()) x=x*10+(c^48);
if (f) x=-x;
}
const int N = 2005;
int h[N], ne[N<<1], to[N<<1], deg[N], tot;
inline void add(int x, int y) {
ne[++tot] = h[x], to[h[x] = tot] = y, deg[x]++;
}
int vis[N], cnt;
void dfs(int x, int f) {
if (x == f || vis[x]) return; vis[x] = 1, cnt--;
for (int i = h[x]; i; i = ne[i]) dfs(to[i], f);
}
int n, m;
int main() {
read(n); cnt = n;
for (int i = 1;i < n; i++) {
int x, y; read(x), read(y);
add(x, y), add(y, x);
}
for (int i = 1;i <= n; i++) {
int j = 1, k;
while (j <= n) {
if (!vis[j] && deg[j] == 1) break;
j++;
}
k = j + 1;
while (k <= n) {
if (!vis[k] && deg[k] == 1) break;
k++;
}
printf ("? %d %d\n", j, k); fflush(stdout);
int w; read(w); dfs(j, w), dfs(k, w);
if (w == j || w == k) deg[w]--;
else deg[w] -= 2;
if (cnt == 1) break;
}
int ans = 0;
for (int i = 1;i <= n; i++) if (!vis[i]) ans = i;
printf ("! %d\n", ans);
fflush(stdout);
return 0;
}
E.Kuroni and the Punishment
贪心构造题, 不妨将构造好的数列排好序, 对于$A_i$来说, 最多有$\frac {i-1}{2}$组$A_j+A_k=A_i$, 这是显然的, 那么直接从1到n排下来, 就是最多有多少个三元组.
现在开始构造, 我先用前k个数拼够m个三元组, 后面的数使其不产生贡献
拼m个三元组, 从1开始放, 在放2, 3... 直到超过m对为止, 最后一个数向右平移可以减少它的贡献, 具体来说还需凑一对, $A_k = (k-1)+(k-2)$,需凑两对$A_k=(k-1)+(k-4), (k-2)+(k-3)$类似这样两两配对即可, 所以就是从前面找二倍的需凑的三元组的个数两两配对就行
后面不产生贡献很好说, 让它们尽量分散, 比如说从一个大质数P开始, 一次递增较小的数d, 两数之差就是$k*d$因为P是大质数, 所以不会出现相等的情况, 我用的1919811(非质数)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
template <typename T>
void read(T &x) {
x = 0; bool f = 0;
char c = getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=1;
for (;isdigit(c);c=getchar()) x=x*10+(c^48);
if (f) x=-x;
}
template <typename T>
void write(T x) {
if (x < 0) putchar('-'), x = -x;
if (x >= 10) write(x / 10);
putchar('0' + x % 10);
}
const int N = 10500;
ll m, n;
ll a[N];
int main() {
read(n), read(m);
ll p;
for (int i = 1;i <= n; i++) {
if (!m) {
p = i; break;
}
ll k = (i - 1) / 2;
if (m >= k) m -= k, a[i] = i;
else {
a[i] = (i - 1) + (i - m * 2);
m = 0; p = i + 1; break;
}
}
if (m) {
cout << -1 << endl;
return 0;
}
a[p] = 1919811; ll k = (a[p-1] == p - 1 ? p : p - 1);
for (int i = p + 1;i <= n; i++) a[i] = a[i-1] + k;
for (int i = 1;i <= n; i++)
printf ("%lld ", a[i]);
return 0;
}
F.Kuroni and the Punishment
这题可以用来开阔思路和视野(竟然还可以这样玩), 国内的题可能没有这么多考察思维的东西
你有n个数, 你可以进行让某个数加一或减一的操作, 求最少的操作次数让这些数最大公因数不为1
奇妙解法:
首先如果让他们都整除二的话最多需操作n次, 这是答案的上限, 其次你发现操作次数不超过1次的数至少有$\frac n2$个, 并且你可以在$\Theta(n)$的时间复杂度内找到让所有数都整除k的最小代价, 考虑随机化, 随便rand一个数操作不超过一次的概率就是$\frac 12$您将$x, x + 1, x - 1$都质因数分解, 把每个质因数都试一遍就可了, 可以多试几次正确率会很高
时间复杂度$\Theta(KnlogN)$
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <algorithm>
#define ll long long
using namespace std;
template <typename T>
void read(T &x) {
x = 0; bool f = 0;
char c = getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=1;
for (;isdigit(c);c=getchar()) x=x*10+(c^48);
if (f) x=-x;
}
template <typename T>
void write(T x) {
if (x < 0) putchar('-'), x = -x;
if (x >= 10) write(x / 10);
putchar('0' + x % 10);
}
const int N = 205000;
ll a[N], ans, n;
map<ll, int> mp;
void check(ll k) {
ll res = 0; if (mp[k]) return; mp[k] = 1;
for (int i = 1;i <= n; i++) {
if (a[i] >= k) res += min(a[i] % k, k - a[i] % k);
else res += k - a[i];
if (res >= ans) return;
}
ans = min(ans, res);
}
void work(ll x) {
for (ll i = 2;i * i <= x; i++) {
if (x % i) continue;
while (x % i == 0) x /= i;
check(i);
}
if (x > 1) check(x);
}
#include <cstdlib>
#include <ctime>
int main() {
freopen ("hs.in","r",stdin);
// srand(time(0));
read(n); ans = n;
for (int i = 1;i <= n; i++) read(a[i]);
random_shuffle(a + 1, a + n + 1);
int lim = min(n, 300ll);
for (int i = 1;i <= lim; i++) {
ll x = a[i];
work(x), work(x + 1); if (x > 1) work(x - 1);
}
cout << ans << endl;
return 0;
}
不srand反而能够(大雾
G.Kuroni and Antihype
n个数$A_i$, 先选一个人进入集合, 然后集合中的人a可以拉集合外的人b, 要满足$A_a & A_b = 0$, 获得利润$A_a$, 求最大利润, $0 \le n, A_i \le 200000$
神仙构造题
可以将拉人的操作看成一棵树, 爸爸拉儿子, 一共拉n-1次, 但不知道第一轮开始让谁进来, 所以建一个n+1人, 它的A值为0, 这样他可以拉任何人进来
然后就是奇妙构造, 将每条边(u, v)的边权设为$A_u + A_v$, 这样整个树的边权和减去所有点权和就是答案
proof: $$ Ans = \sum_{i=1}^nA_i*(deg(i)-1)=\sum_{i=1}^nA_i*deg(i)-\sum_{i=1}^nA_i $$ 然后就是跑最大生成树了, 可以知道最多有$3^{18}$条边, 因为A是小于$2^{18}$的, 可以用二项式定理证明边数, 然后克鲁斯卡尔就行了
这样可过, 但我们还有更优的做法, 暂时没看懂, 待填坑
Find MST with Borůvka's algorithm. Let's do logn iterations of the following:
For each mask, find two largest present weights (from different components) which are submasks of this mask in O(218⋅18) with SOS DP. Then, for each component we can find the edge from this component to some other component with the largest weight, and do one iteration of Boruvka. Complexity O(218⋅18⋅log(n)).
来源:oschina
链接:https://my.oschina.net/u/4409146/blog/3306742