Codeforces Round #628 (Div. 2)

题目链接:https://codeforces.com/contest/1325

A - EhAb AnD gCd

题意:给一个正整数 \(s(2 \leq s \leq 10^9)\) ,构造两个正整数 \(a,b\) ,使得 \(gcd(a,b)+lcm(a,b)=s\)

题解:设 \(a=xg,b=yg\)\(gcd(x,y)=1\) ,则 \(gcd(a,b)=g,lcm(a,b)=\frac{ab}{g}=xyg\) ,原式为 \((xy+1)g=s\) ,所以说只要分解出 \(s\) 的一个因子 \(g(g\neq s)\) ,就可以构造出来。易知可以取 \(g=1\)

B - CopyCopyCopyCopyCopy

签到题。

题意:给一个 \(n\) 个数的数组,然后可以把这个数组复制粘贴 \(n\) 次(例如把 \([1, 2, 3]\) 复制为 \([1, 2, 3, 1, 2, 3, 1, 2, 3]\))。求复制粘贴之后的数组的最长上升子序列的长度。

题解:思考一下复制粘贴一次之后,最长上升子序列的变化,意思就是可以从头再选一次元素,那么很容易想到,只需要在每次复制粘贴的数组里面选一个尽可能小的元素,就可以弄出最长上升子序列,所以答案就是排序去重。

*C - Ehab and Path-etic MEXs

题意:给一棵 \(n\) 个节点的树,要求给每条边赋权值 \([0, n-1]\) 。对于某个点对 \((u,v)\) ,定义 \(mex_path(u,v)\) 的值就是简单路径 \((u,v)\) 中经过的边的权值的集合的 \(mex\) ,就是那个SG函数里面用到的 \(mex\) ,返回值是这个集合中不存在的最小的自然数,例如 \(mex({1,2,3,5})=0\)\(mex({0,1,2,4,5})=3\) 。要求赋值的结果,最小化所有的点对 \((u,v)\) 的最大 \(mex_path(u,v)\)

题解:思考了很久不知道怎么弄。不过后面突然想到了,是要使得权值小的边,尽可能不被大多数的路径经过?题目是要求最小化最大值,又不是最小化和。但是这个思路确实有启发。突然想到假如有一个“三岔路口”,那么假如给三条边赋值为 \([0,1,2]\) ,那么显然每条简单路径至多经过其中的两条,也就是说 \(mex_path\) 值至多为 \(2\) 。只需要说明 \({0,1}\) 不能被构造出来就能够证明 \(2\) 是最小值了。幸运的是,显然至少有一条边的 \(mex_path\) 值为1,就是那条权值为 \(0\) 的边。而只要 \(n\geq 2\) ,就一定有一条边,那条权值为 \(0\) 的边无论如何都会存在,所以 \(0\) 不能被构造。同理,假如有 \(n\geq 3\) ,那么那条同时包含权值为 \(0,1\) 两条边的路径的 \(mex_path\) 值至少为 \(2\) 。易知“三岔路口”至少需要4个点,所以拥有4个点的,且拥有“三岔路口”的,都可以通过这个方法构造。除此之外可以显然得到至多3个点的或者虽然点很多但依然是一条链的,肯定有一条简单路径是包含所有的边的,所以随便怎么构造都是对的。

当时写得比较冗余,其实这几种构造是可以合并在一起的,只需要取出度最多的点,尝试给其中的边分配 \([0,1,2]\) (或者度最多的点不够,那也是分配最小的几个给他),其他的边是乱分配即可。

vector<int> G[100005];
int ans[100005];

void test_case() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(i);
        G[v].push_back(i);
    }
    int id = 1;
    for(int i = 1; i <= n; ++i) {
        if(G[i].size() >= 3) {
            id = i;
            break;
        }
    }
    int top = 0;
    for(auto &i : G[id])
        ans[i] = ++top;
    for(int i = 1; i <= n - 1; ++i) {
        if(ans[i] == 0)
            ans[i] = ++top;
        printf("%d\n", ans[i] - 1);
    }
}

*D - Ehab the Xorcist

题意:给定非负整数 \(u,v(0\leq u,v \leq 10^18)\) ,求最短的正整数数组,使得数组中所有元素的异或和为 \(u\) ,和为 \(v\) 。注意,若数组中没有元素,则异或和和和都是 \(0\)。无解输出 \(-1\)

题解:既然是最短,那么考虑哪些东西会是 \(0\) ,由于题目要求的都是正整数,所以只有当 \(v=0\) 的时候才有可能需要 \(0\) 个数,这时必定要 \(u=0\) ,否则若 \(v=0\)\(u > 0\) ,直接无解。

那么从这里开始就有 \(v>0\) ,一种很显然的情况也是无解的,就是 \(u>v\) (前面的 \(v=0\)\(u>0\) 也是这种情况)。要构造 \(u\) 的每个某个位的1,那么至少需要这个位的1这么大,所以和是不可能少于 \(v\) 的。其次有一种情况也是显然无解的,就是 \(u,v\) 的奇偶性不同,因为假如 \(v\) 是偶数,那么就需要偶数个个位1,但是偶数个个位1异或的结果必定是 \(0\) 。反之同理。

这时可以讨论掉答案为 \(1\) 的情况,显然这种情况的充要条件是 \(u=v\) ,根据上面已经把都是0的情况去掉了,所以这里不再判断0。

那么答案至少是 \(2\)

这种题目一般都会和 \(u\) 扯上什么关系,会不会先固定一个数是 \(u\) 呢?不妨先固定第一个数是 \(u\) ,那么剩下的数只需要异或互相抵消掉(相等),且和为 \((v-u)\) 。这时通过“ \(u<v\)\(u,v\) 的奇偶性相同”可以得到 \((v-u)\) 是偶数,那么显然可以设 \(x=\frac{v-u}{2}\) ,那么 \({u,x,x}\) 就是一个答案为 \(3\) 的解。实验证明这种题目先固定掉一个与“异或”关联的条件总是凑效的,或许因为“异或”这个运算比起加法运算要简单,不涉及进位。

那么假如 \(u,x\) 可以合并在一起,就可以弄到答案是 \(2\) 的结果了,能够把两个数字合并在一起,就要求前后不发生影响,易知当 \(u,x\) 的二进制1都互相错开,那么可以合并在一起,换言之要求 \(u\;and\;x=0\)

不过怎么说明 \(u\;and\;x \neq 0\) 的情况,不可以构造出答案是 \(2\) 呢?比赛的时候没想出来,最后乱蒙一个就上了。没想到还真是这样。

官方的题解指出,可以通过尝试构造来验证不可能答案是 \(2\) ,过程如下:

已知 \(u\;and\;x \neq 0\) ,假设答案是 \(2\) ,不妨设构造出的两个数为 \(a,b\) ,这时满足 \(a \;xor\; b=u\) ,且 \(a+b=v\) 。官方题解给出了一个从未见过的等式: \(a+b=a \;xor\; b + 2(a\;and\;b)\) ,意思就是错开的二进制位直接保留1,都是0的位不用管,而都是1的位,则会产生一个进位。也就是说 \(v=u+2(a\;and\;b)\) 变形得 \(a\;and\;b=x\) ,代入已知得 \((a\;xor\;b)\;and\;(a\;and\;b)\neq 0\) ,要使得这个方程成立,需要某个二进制位的结果使得左边为1,否则若左边的每个二进制位都是0,那么答案就是0。考虑一个二进制为,对 \(a,b\) 枚举所有的情况,显然发现只有当 \(a=1,b=1\) 的情况才有 \((a\;and\;b)=1\) ,此时左边却为 \(0\) ,所以矛盾。

ll u, v;
 
void test_case() {
    scanf("%lld%lld", &u, &v);
    if(u == v) {
        if(u == 0) {
            puts("0");
            return;
        }
        puts("1");
        printf("%lld\n", u);
        return;
    }
    if(u > v) {
        puts("-1");
        return;
    }
    if((u & 1) != (v & 1)) {
        puts("-1");
        return;
    }
    ll tmp = (v - u) >> 1;
    if((tmp ^ u) == (tmp | u)) {
        puts("2");
        printf("%lld %lld\n", (tmp | u), tmp);
        return;
    }
    puts("3");
    printf("%lld %lld %lld\n", u, tmp, tmp);
}

*E - Ehab's REAL Number Theory Problem

题意:从 \(n(1\leq n\leq 10^5)\) 个正整数的数组 \(a\) 中,选出最短的一个子序列,使得这个子序列的乘积是完全平方数。每个数 \(a_i (1\leq a_i \leq 10^6)\)\(a_i\) 至多有 \(7\) 个因子。

题解:最诡异的限制条件是“ \(a_i\) 至多有 \(7\) 个因子” ,仔细推一推发现这个意思就是 \(a_i\) 至多有两种不同的质因子。然后我当时就考虑特判掉 \(1\) 以及两个相等的数的情况,然后是一些 \(p\) ,一些 \(p*q\) ,和一些 \(p*q*q\) 之间找一条首尾都是同种单数个因子的路径的,这个图都不知道怎么建,连路径都不一定找得到,别说是最短路径了。所以这道题就是完全不会的。

其实一个显然的思路是这样,因为 \(1\) 也是一个完全平方数,所以可以把所有其他的平方因子约掉,不影响答案,对每个数约掉平方因子,得到的结果只能是 \(1,p,p*q\) 三种类型。考虑对 \(p\*q\) 这种类型的数,在图中连接 \((p,q)\) ,考虑选择一个数对应走一条边,那么一个乘积是完全平方数的子序列就是一条路径,可以观察路径 \(path(x,y)\) 中,除了 \(x,y\) 只被走了一次,其他的都被走了两次,必定是平方因子,且子序列的长度就是路径的边数,要求整个乘积都是平方的因子就要 \(x=y\) ,换言之就是要求最短环。仔细想想这种情况可以推广到 \(1,p\) 的情况,首先可以特判掉 \(1\) ,这种数自己就是一个完全平方数且不可能有东西比它更短,假如给 \(1\) 连接一个自环,也可以合并到这种情况,当然特判掉更简单。对于 \(p\) ,假如有两个 \(p\) ,在特判掉 \(1\) 之后,答案是 \(2\) 的就是最短子序列,也可以继续特判。否则这个 \(p\) 是没得配的(错误,例如:2 2*3 3*5 5*7 7)。够无聊的话,也可以连接 \((1,p)\) 这样有两个 \(p\) 的时候就会走一次1然后回来,也是最小环(提示我们有时候可以把 \(1\) 看作“质数”)。那么剩下的这些 \(p*q\) 的最小环怎么算呢?我昨晚只知道一种 \(O(n^3)\) 的Floyd算法,题解和qls给出了一种 \(O(n^2)\) 的算法,首先枚举每个要求环的点 \(i\) ,然后以 \(i\) 为源点进行一次bfs,找出整棵bfs树。当某条非树边可以“追溯”(就是求双连通分量的那个回溯,指通过非树边去到另一棵子树,然后沿子树的父亲上升到 \(i\) 点)到 \(i\) 点,那么这条非树边就可以和bfs树上的东西形成一个环,这些环就是包含 \(i\) 点的环。注意这条“非树边”,既不可能可能是“前向边”,也不可能是“后向边”(虽然有可能指向父亲,但毕竟这是无向图,这时是“树边”的反边),也有可能是横叉边(连接同一层的不同节点,甚至是某个节点的自环)。

具体来说,假如当前节点是 \(u\) ,边指向的节点是 \(v\) ,若 \(v\) 已经被访问过,且 \(dep[u]>dep[v]\) ,则这个一定是“树边”的反边或者“横叉边”,且一定有 \(dep[u]=dep[v]+1\) ,直接跳过(虽然横叉边可以更新答案,但是可以交给另一端来更新);若 \(v\) 已经被访问过,且 \(dep[u]=dep[v]\) 则这个一定是“横叉边”,可以更新环为 \(dep[u]+dep[v]+1\) ;否则, \(v\) 已经被访问过,且 \(dep[u]<dep[v]\) 则这个一定是“横叉边”,而且一定满足 \(dep[u]+1=dep[v]\) (若不然,按照bfs的性质,\(v\) 的父亲就应该是 \(u\) ) ,可以更新环为 \(dep[u]+dep[v]+1\)

注意到其实无向图bfs中,边连接的点的深度改变都是只有1(经过assert验证)。为什么没有“前向边”?因为bfs的次序决定了,在取出这个目标节点的时候,要么它在其他搜索树中搜索过了,变成了“横叉边”,要么没有搜索过,变成了“树边”。同理也不会有“后向边”,或者说“后向边”一定变成是某条树边的反边。

解决了怎么求最小环之后,问题在于 \(O(n^2)\) 还是太大了。这里要惊人地注意到,由于 \(a_i \leq 10^6\) ,那么所有 \(p*q(p<q)\) 这种形式的数,必有小的那个因子\(p (p<10^3)\) 。所以不需要考虑从 \(q(q>10^3)\) 出发的最小环,因为从 \(q\) 出发走第一步是不能走到其他 \(q'(q'>10^3)\) 的,必须要往小的 \(p\) 走第一步。否则,若连接有 \((q,q')\) ,则说明有一个数 \(a_i\geq q*q' >10^6\)

也就是说,最小环若存在必定包含至少一个 \(p(p<10^3)\) ,那么只需要对 \(p(p<10^3)\) 进行bfs即可。

const int MAXN = 1e6;
int p[MAXN + 5], ptop;
int pm[MAXN + 5], pid[MAXN + 5];

void sieve() {
    int n = MAXN;
    p[0] = 1;
    pm[1] = 1;
    pid[1] = 0;
    for(int i = 2; i <= n; ++i) {
        if(!pm[i]) {
            p[++ptop] = i;
            pm[i] = i;
            pid[i] = ptop;
        }
        for(int j = 1; j <= ptop; ++j) {
            int t = i * p[j];
            if(t > n)
                break;
            pm[t] = p[j];
            if(i % p[j] == 0)
                break;
        }
    }
}

int a[100005];

int fj(int x) {
    int d = 1;
    while(x > 1) {
        int p = pm[x];
        x /= p;
        if(d % p == 0) {
            d /= p;
            continue;
        }
        d *= p;
    }
    return d;
}

vector<int> G[1000005];

int ans;
int dep[1000005];
queue<int> Q;

void bfs(int s) {
    for(int i = 0; i <= ptop; ++i)
        dep[i] = -1;
    while(!Q.empty())
        Q.pop();
    dep[s] = 0;
    Q.push(s);
    while(!Q.empty()) {
        int u = Q.front();
        Q.pop();
        for(auto &v : G[u]) {
            if(dep[v] == -1) {
                dep[v] = dep[u] + 1;
                if(dep[v] + dep[u] + 1 >= ans)
                    continue;
                Q.push(v);
                continue;
            }
            if(dep[v] >= dep[u]) {
                ans = min(ans, dep[v] + dep[u] + 1);
                continue;
            }
        }
    }
}

void test_case() {
    int n;
    scanf("%d", &n);
    sieve();
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        a[i] = fj(a[i]);
    }
    sort(a + 1, a + 1 + n);
    if(a[1] == 1) {
        puts("1");
        return;
    }
    int m = unique(a + 1, a + 1 + n) - (a + 1);
    if(m < n) {
        puts("2");
        return;
    }
    for(int i = 1; i <= m; ++i) {
        int x = pid[pm[a[i]]], y = pid[a[i] / pm[a[i]]];
        G[x].push_back(y);
        G[y].push_back(x);
    }
    ans = INF;
    for(int i = 0; i <= ptop && p[i] <= 1000; ++i)
        bfs(pid[p[i]]);
    if(ans == INF)
        ans = -1;
    printf("%d\n", ans);
}

收获:
1、考虑“最小质因子”的取值,经常可以使得复杂度退化到根号,也注意那种不能分成两个质因子的数,是否和那些可以分成两个质因子的数不一样,可以分开处理。
2、把1看作第0个质数。这样那种 \(p_1\) 类型的和 \(p_2\) 类型的就可以通过1连接成一个环。否则,要特殊处理两端分别是 \(p_1\)\(p_2\) 的情况,注意到中间确实还是会经过一些小的质数,所以复杂度不会改变。具体的做法应该是在枚举小质数作为源点的时候,若更新到含有单个 \(v\) 的节点 \(v\) 则记录一个最小深度+1。若找到两个这样的单个的 \(v_1,v_2\) ,就把这个值拿去更新答案。由前面排序去重可知,找到的 \(v_1,v_2\) 必定不同。
3、图的最小环,可以从每个点开始跑一次bfs,得到包含这个点的最小环。具体的做法是拿bfs中的“非树边”,也就是“横叉边”来接通两棵树。(若没有经过排序去重,则有时候这个“横叉边”有可能以多重边的形式存在,例如2 6 6,都是从2走到3,遍历s的时候把3推入队列中两次,这时要把从2(源点)出发的不同的多重边(2,3),看作不同的子树,这样就依然是“横叉边”)。“横叉边”连接的是已遍历的节点,他可能连接同一层的节点,也可能连接下一层的节点,也可能连接上一层的节点。注意区分“树边”是只能连接下一层的未遍历节点,或者上一层的已遍历节点(父亲)。这时,若仅当某条边访问已遍历的同一层或者下一层的节点,就可以只取出“横叉边”,其中遍历同一层的“横叉边”被取出两次,连接不同层的“横叉边”只取出一次。
4、天大的bug,在bfs里面清空dep数组的时候,忘记改成离散化坐标/忘记对应原坐标,这种bug通过神奇的assert检测出来了,意思是可以在区域赛拼命交assert,找到那个RE的点?这里是因为assert一个 \(dep[v]<dep[u]\) 等价于 \(dep[v]=dep[u]-1\) 失败发现的。

*F. Ehab's Last Theorem

前一题是无向图的bfs,无向图的bfs是没有“前向边”和“后向边”的,只有“树边”和“横叉边”。

无向简单图bfs
若指向的节点未被访问:
是“树边”。

若指向的节点已经被访问:
指向深度较小的节点的边,要么是“树边”的反边,要么是“横叉边”,可以通过记录bfs树上的父亲来判断。
指向深度较大的节点的边,是“横叉边”。
指向深度相等的节点的边,是“横叉边”。

这一题是无向图dfs,无向图的dfs是没有“横叉边”的(在结束对这个子树的搜索之前,不会去访问下一个子树),“前向边”(或者“树边”,假如是父子关系)和“后向边(或者“树边”的反边,假如是父子关系)”成对出现。

无向简单图dfs
若指向的节点未被访问:
是“树边”。

若指向的节点已经被访问:
指向深度较小的节点的边,要么是“树边”的反边,要么是“后向边”,可以通过记录dfs树上的父亲来判断。
指向深度较大的节点的边,是“前向边”。
不可能指向深度相等的节点。

参考资料:[Tutorial] The DFS tree and its applications: how I found out I really didn't understand bridges

题意:给一个 \(n\) 个点, \(m\) 条边的无向简单图,记 \(sq=\lceil \sqrt{n} \rceil\) ,要求找一个长度大于等于 \(sq\) 的简单环(A simple cycle is a cycle that doesn't contain any vertex twice. 也就是没有“8”字形的,或者说“8”字形的是两个简单环),或者找一个大小 恰好\(sq\) 的独立集。

题解:记 \(sq=\lceil \sqrt{n} \rceil\) 从任意一个点开始dfs,取出dfs树。注意区分“树边”的反边和“后向边”的区别,在这个无向简单图中,指向自己父亲的边,就是“树边”的反边,是不能算作“后向边”的。若某个点拥有不少于 \(sq-2\) 条“后向边”,那么选择“后向边”指向的点中深度最小的点走回去,就找到了一个长度不少于 \(sq\) 的环,因为是无重边无自环的简单图, \(sq-2\) 条“后向边”意味着有 \(sq-2\) 个不同的非父亲祖先,加上父亲和自己就有 \(sq\) 个点了。否则,若不存在这样的环,从dfs树中深度最大的点开始选择独立集。首先先把深度最大的点给选了,然后其“后向边”指向的非父亲祖先和其父亲都不能再选,由于选择某个点至多会导致其至多 \(sq-1\) 个祖先不能再选,那么每步选择最多会减少 \(sq\) 个点,显然这样是可以选到不少于 \(sq\) 个点的独立集的。

理清一下 \(sq\) 中上整的问题,比如 \(n=7\) ,那么 \(sq=3\) ,意味着选一个点至多会总共减少 \(sq=3\) 个点,选了2个之后最后还是会剩下至少1个点,所以可以选 \(sq\) 次。

具体实现的时候也需要从深度最大的点开始选,否则搞一个菊花图,就卡掉了。而且发现在实现的时候不需要区分“树边”的反边和“后向边”的区别。

int n, m, sq;
vector<int> G[100005];
int dep[100005];
int pa[100005];

pii tmp[100005];
int vis[100005];

void dfs(int u, int p) {
    dep[u] = dep[p] + 1;
    pa[u] = p;
    int cnt = 1, mindepv = u;
    for(auto &v : G[u]) {
        if(dep[v] == 0) {
            dfs(v, u);
            continue;
        }
        if(dep[v] < dep[u]) {
            ++cnt;
            if(dep[v] < dep[mindepv])
                mindepv = v;
        }
    }
    if(cnt >= sq) {
        cnt = 0;
        int cur = u;
        ++cnt;
        while(cur != mindepv) {
            cur = pa[cur];
            ++cnt;
        }
        printf("2\n%d\n", cnt);
        printf("%d", u);
        cur = u;
        while(cur != mindepv) {
            cur = pa[cur];
            printf(" %d", cur);
        }
        printf("\n");
        exit(0);
    }
}

void test_case() {
    scanf("%d%d", &n, &m);
    for(sq = 1; sq * sq < n; ++sq);
    while(m--) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, 0);
    for(int i = 1; i <= n; ++i)
        tmp[i] = {dep[i], i};
    sort(tmp + 1, tmp + 1 + n, greater<pii>());
    printf("1\n");
    int cnt = 0;
    for(int i = 1; i <= n; ++i) {
        int u = tmp[i].second;
        if(vis[u])
            continue;
        if(cnt != 0)
            printf(" ");
        printf("%d", u);
        vis[u] = 1;
        for(auto &v : G[u])
            vis[v] = 1;
        ++cnt;
        if(cnt == sq) {
            printf("\n");
            exit(0);
        }
    }
}
posted @ 2020-03-15 10:53  KisekiPurin2019  阅读(200)  评论(0编辑  收藏  举报