Ozon Tech Challenge 2020 (Div.1 + Div.2)
A
排序输出即可。
B
题目要求的是删除尽可能少的次数使得剩余的串无法被删除,那么其实问题关键在于尽可能少的次数
。我们可以从开头和结尾向中间删括号,遇到一对匹配的括号就将其删去。这样子操作可以使那些嵌套的括号们形成)(
的结构,从而保证了次数最少且剩下的串无法继续删括号。
C
看起来很难的样子;但是只要注意到了模数范围1e3
与乘法
运算的特点就可以容易想到正解。
如果出现的元素超过m个,那么由抽屉原理一定有若干个元素模m余数相等,这些元素的差值模m就为0,因而可以直接得出答案为0的结论;如果出现元素不超过m个,那么暴力搜索 O ( n 2 ) O(n^2) O(n2)即可。
D
第一次做到的交互题。
读入读写其实根据提示及时的刷新输出流即可,考察的还是算法思维。
因为至多查询 f l o o r ( n / 2 ) floor(n/2) floor(n/2)次,所以每次查询应该要查询两个新点,不然就会造成查询浪费导致无法推断出根的位置。
自己写的时候考虑的不是很完善,采用了查询相邻两个点
来判断根的策略;但是这种方法在某些情况下无法确定根的位置(直到最后我才意识到这个问题!!)…
一种正确的查询方法是每次查询两个新出现的叶子,这里定义那些度为1的点为叶子。如果这对叶子的LCA是叶子之一,那么该LCA叶子显然就是整棵树的根;如果不是的话,删去两个叶子,更新相邻点的度数,再搜索两个叶子重复上述过程即可。
如果有奇数个节点的话有可能最后剩余1个节点未被访问,此时找不到一对叶子,应该遍历所有的节点找唯一一个没有被访问的位置,该位置即为根节点。
代码
const int maxn = 1e3 + 10;
int n;
struct star{int to, next;};
int head[maxn], top = 0;;
star edge[maxn << 1];
int vis[maxn], du[maxn];
void add(int u, int v){
edge[top].to = v;
edge[top].next= head[u];
head[u] = top++;
}
int query(int u, int v){
printf("? %d %d\n", u, v); fflush(stdout);
int w; scanf("%d", &w);
return w;
}
void ans(int x){ printf("! %d\n", x);}
int isleaf(int x){ if(du[x] == 1) return 1; return 0;}
void del(int x){for(int i = head[x]; ~i; i = edge[i].next) du[edge[i].to]--;}
//从x出发开始,找2个叶子。
vector<int> lf;
int dfs(int x){
if(isleaf(x)){
lf.push_back(x); vis[x] = 1;
}
for(int i = head[x]; ~i; i = edge[i].next){
if(!vis[edge[i].to] && isleaf(edge[i].to) && lf.size() <= 1){
lf.push_back(edge[i].to);
vis[edge[i].to] = 1;
}
}
if(lf.size() == 2){
int w = query(lf[0], lf[1]);
del(lf[0]); del(lf[1]);
if(w == lf[0] || w == lf[1]){
ans(w); return 2;
}
return 1;
}
else for(int i = head[x]; ~i; i = edge[i].next){
if(!vis[edge[i].to]){
vis[edge[i].to] = 1;
int sts = dfs(edge[i].to);
vis[edge[i].to] = 0;
if(sts) return sts;
}
}
return 0;
}
int main(){
// Fast;
memset(head, -1, sizeof head); scanf("%d", &n);
for(int i = 0, x, y; i < n - 1; i++){
scanf("%d %d", &x, &y);
add(x, y); add(y, x);
du[x]++; du[y]++;
}
while(true){
int id; for(id = 1; id <= n; id++) if(!vis[id]) break;
lf.clear();
int sts = dfs(id);
if(sts == 2) return 0;
if(sts == 0) break;
}
for(int i = 1; i <= n; i++) if(!vis[i])
ans(i);
return 0;
}
E
E题不算很难,考察序列构造手法。
可是D做不出来心态崩了哪里高兴看E
因为对于某个位置 i > = 3 i >= 3 i>=3, 它最多可能参与 f l o o r ( i − 1 2 ) floor(\frac{i-1}2) floor(2i−1)组三元组等式,即 ( 1 , i − 1 ) , ( 2 , i − 2 ) , … ( i − 1 2 , i + 1 2 ) (1, i-1), (2, i-2),\dots(\frac{i-1}2, \frac{i + 1}2) (1,i−1),(2,i−2),…(2i−1,2i+1)。而我们又可以构造出一组最大情况,即 1 , 2 , 3 , … , n 1,2,3,\dots,n 1,2,3,…,n
那么我们就可以根据该序列进行微调使得三元组组数为要求的 m ∈ 1 e 9 m∈1e9 m∈1e9 :
首先构造正整数序列 1 , 2 , … , i − 1 1,2,\dots, i-1 1,2,…,i−1, 满足加入 i i i后三元组组数第一次大于 m m m; 之后我们根据剩余的组数 l e f t = n − i + 1 left = n-i+1 left=n−i+1选择之后 n − i + 1 n-i+1 n−i+1个元素的位置。因为剩余 l e f t left left,所以我们将之后一个元素放在
( i − 1 ) + ( i − 2 ∗ l e f t ) = 2 ∗ i − 1 − 2 ∗ l e f t (i-1)+(i-2*left)=2*i-1-2*left (i−1)+(i−2∗left)=2∗i−1−2∗left
放完该元素之后要求的组数就已经全部构造完成了,接下来就是构造一些冗余项填满 n n n个位置。因为最多5000个元素,并且最大不超过 1 e 8 1e8 1e8, 所以加入1e8+5000+i
即可填满所有位置且不产生新的三元等式组。
代码
int n, m;
vector<ll> ans;
int main(){
// Fast;
scanf("%d %d", &n, &m);
if(n == 1 || n == 2){
if(m) printf("-1");
else for(int i = 0; i < n; i++) printf("%d ", i + 1);
printf("\n");
return 0;
}
ll M = 0;
for(int i = 3; i <= n; i++) M += (i - 1) / 2;
if(m > M) puts("-1");
else{
ans.push_back(1); ans.push_back(2);
int now = 3;
while(m){
if(m - (now - 1) / 2 >= 0){
m -= (now - 1) / 2;
ans.push_back(now++);
}
else{
ans.push_back(now + now - 1 - 2 * m);
break;
}
}
int top = (int)ans.size();
int cnt = 1;
while(top < n){
ans.push_back(100000000 + 5010 * cnt);
top++; cnt++;
}
for(auto i: ans) printf("%lld ", i); printf("\n");
}
return 0;
}
F
因为至多只需要操作n次,就一定可以完成将整个序列变为2的倍数,所以尝试写了一发暴力枚举TLE7…
正确的做法是:
因为至多只需要操作n次,所以可以推出在最优操作情况下被操作不超过1次的元素至少有n/2个。
那么对于某一个位置的元素 x x x 而言, x − 1 , x , x + 1 x-1,x,x+1 x−1,x,x+1中包含最优素数的可能性至少为1/2。
(最终序列的公因子是一个素数肯定优于是一个合数的情况,而在素数中使得操作数最少的那个素数就被称为最优素数)
所以我们可以随机取若干个不同的位置,用这些元素的因子去更新答案,随机的次数越多找到最优素数的可能性越大。如果取随机次数为10,失败几率也只有 0.1 0.1% 0.1,经过测试确实可以AC,跑的飞快。
有两个注意点:
AC代码
偷了个引擎 没好好学C++不会用
const int maxn = 1e6 + 10;
int n;
ll a[maxn];
ll prime[maxn], top = 0;
int vis[maxn];
mt19937_64 mt(chrono::steady_clock::now().time_since_epoch().count());
void getprime(){
for(int i = 2; i <= 1000000; i++){
if(!vis[i]) prime[top++] = i;
for(int j = 0; j < top && i * prime[j] <= 1000000; j++){
vis[i * prime[j]] = 1;
if(i % prime[j] == 0) break;
}
}
}
set<ll> pp;
void add(ll x){
if(x <= 1) return;
for(int i = 0; i < top && prime[i] <= x; i++){
if(x % prime[i] == 0){
pp.insert(prime[i]);
while(x % prime[i] == 0) x /= prime[i];
}
}
if(x != 1) pp.insert(x);
}
int main(){
// Fast;
getprime();
scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%lld", a + i); shuffle(a, a + n, mt);
for(int i = 0; i < 10; i++){
add(a[i]); add(a[i] - 1); add(a[i] + 1);
}
ll ans = n, res;
for(auto i: pp){
res = 0;
for(int j = 0; j < n; j++){
if(a[j] < i) res += i - a[j];
else res += min(i - a[j] % i, a[j] % i);
if(res > ans) break;
}
ans = min(ans, res);
}
printf("%lld\n", ans);
return 0;
}