Ozon Tech Challenge 2020 (Div.1 + Div.2) (A-F)

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(2i1)组三元组等式,即 ( 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,i1),(2,i2),(2i1,2i+1)。而我们又可以构造出一组最大情况,即 1 , 2 , 3 , … , n 1,2,3,\dots,n 1,2,3,,n
那么我们就可以根据该序列进行微调使得三元组组数为要求的 m ∈ 1 e 9 m∈1e9 m1e9 :
首先构造正整数序列 1 , 2 , … , i − 1 1,2,\dots, i-1 1,2,,i1, 满足加入 i i i后三元组组数第一次大于 m m m; 之后我们根据剩余的组数 l e f t = n − i + 1 left = n-i+1 left=ni+1选择之后 n − i + 1 n-i+1 ni+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 (i1)+(i2left)=2i12left
放完该元素之后要求的组数就已经全部构造完成了,接下来就是构造一些冗余项填满 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 x1,x,x+1中包含最优素数的可能性至少为1/2。
(最终序列的公因子是一个素数肯定优于是一个合数的情况,而在素数中使得操作数最少的那个素数就被称为最优素数)
所以我们可以随机取若干个不同的位置,用这些元素的因子去更新答案,随机的次数越多找到最优素数的可能性越大。如果取随机次数为10,失败几率也只有 0.1 0.1% 0.1,经过测试确实可以AC,跑的飞快。

有两个注意点:

  • 不要用rand取随机下标进行访问,因为如果下标随机到了相同的数字就会使得有效随机数减少。应该使用shuffle打乱序列取若干个元素,这样就保证了有效随机数不会缩小。
  • 应该先将所有的可能的素数用set维护,最后再在整个序列中进行更新操作;不然,如果给出的序列为拥有最多因子的相同数字,就会TLE。

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;
}

你可能感兴趣的:(ACM)