Educational Codeforces Round 2

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

A - Extract Numbers

一个很无聊的模拟,难度居然是1600,看来当时的竞赛和现在完全不一样。

B - Queries about less or equal elements

排个序二分一下,很无聊的一个题。注意应该使用upper_bound。

*C - Make Palindrome

一开始以为是随便贪心一下的无聊题,结果WA15了。

题意:给一个串s,可以对它进行重排,然后修改其中的一些字母。修改最少的次数使得他变成回文串,假如修改次数最少的有多种方案,求结果字典序最小的一种。

题解:假如串是奇数长度,那么可以容纳一个奇数字母,当然选择剩下的容纳最小的字母。然后想怎么构造两边,假如最小的字母还有剩,那么肯定放一个在左边,然后假如还有剩,再放一个在右边,continue。否则一定要进行一次修改,为了使得字典序最小,就应该找出现次数是奇数的最大的字母,把他变成最小的字母,然后放进去,continue。

当然写的时候就不需要这么蠢了。用cnt的方法写,注意要跳过奇数次的字母。

char s[200005];
int cnt[128];

void test_case() {
    scanf("%s", s + 1);
    int n = strlen(s + 1);
    for(int i = 1; i <= n; ++i)
        ++cnt[s[i]];
    int l = 'a', r = 'z';
    while(l < r) {
        if(cnt[l] % 2 == 0) {
            ++l;
            continue;
        }
        if(cnt[r] % 2 == 0) {
            --r;
            continue;
        }
        ++cnt[l];
        --cnt[r];
        ++l;
        --r;
    }
    int c = 'a';
    for(int i = 1; i <= n / 2; ++i) {
        while(cnt[c] < 2)
            ++c;
        s[i] = c;
        s[n - i + 1] = c;
        cnt[c] -= 2;
    }
    c = 'a';
    if(n % 2 == 1) {
        while(cnt[c] == 0)
            ++c;
        s[(n + 1) / 2] = c;
    }
    puts(s + 1);
}

*D - Area of Two Circles' Intersection

题意:给两个圆,求面积交。

从现在这个年代看好像这种题很模板,可能当时的人水平和现在不一样吧。不过自己从来没有认真推过这个结论,不妨现在看一看。

题解:特判掉两圆内含、两圆外离(含相切)的情况,剩下的就是相交,相交部分为两个不见得一定是一样的弓形,要计算这个弓形的面积,应该是使用扇形的面积减去三角形的面积,换言之只需要知道交点和圆心连线的圆心角的大小。画了一下发现原来这种圆心角也有可能是优角,这是之前没有观察到的。

怎么找交点的坐标呢?仔细想想甚至还真不需要!好像之前在学校的题目里面做过,这里已经知道了两圆的半径,也很容易根据圆心算出圆心距,那么两个圆心和交点形成的三角形就是唯一的(SSS全等),可以用余弦定理解出圆心角的一半的cos值,然后计算反cos得到圆心角的一半。注意到这种算法并不关心圆心角是否是优角,甚至这个算法包含了外离和内含的情况(不能构成三角形)。这种算法的误差来源:计算cos值时不可避免求了模的根号,使用内置函数求反cos值,以及后面的一系列浮点运算产生的近似,关键是没有二分求解交点这样的误差非常大的环节。也省去了对于不同情况下找交点的讨论。

注:三角形的面积,这里没有点的坐标,用正弦定理的推论算出来。

void test_case() {
    ll x1, y1, r1;
    ll x2, y2, r2;
    scanf("%lld%lld%lld", &x1, &y1, &r1);
    scanf("%lld%lld%lld", &x2, &y2, &r2);
    long double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    if(r1 + r2 <= dis) {
        printf("%.12f\n", (double)(0.0));
        return;
    }
    if(r1 + dis <= r2) {
        printf("%.12f\n", (double)(PI * r1 * r1));
        return;
    }
    if(r2 + dis <= r1) {
        printf("%.12f\n", (double)(PI * r2 * r2));
        return;
    }

    long double cosa1 = (dis * dis + r1 * r1 - r2 * r2) / (2.0 * r1 * dis);
    long double a1 = acos(cosa1);
    long double A1 = 2.0 * a1;
    long double S1 = 0.5 * r1 * r1 * A1 - 0.5 * sin(A1) * r1 * r1;

    long double cosa2 = (dis * dis + r2 * r2 - r1 * r1) / (2.0 * r2 * dis);
    long double a2 = acos(cosa2);
    long double A2 = 2.0 * a2;
    long double S2 = 0.5 * r2 * r2 * A2 - 0.5 * sin(A2) * r2 * r2;

    long double S = S1 + S2;

    printf("%.12f\n", (double)(S));
}

收获:才发现公式全忘了,余弦定理是 \(a^2=b^2+c^2-2bc \cos A\) ,正弦定理的推论是 \(S=\frac{1}{2}ab \sin C\)圆心角所对的弧长\(l=\alpha r\)圆心角所对的扇形的面积\(S=\frac{1}{2}\alpha r^2\)

思考:那么既然能够算出圆心角的大小,就可以通过构造一个圆心连线的向量,然后顺时针或者逆时针Rotate过去,再加上圆心的坐标,就可以得到交点的坐标。

*E - Lomsat gelral

题意:给一棵以1为根的n个节点的树,每个节点有个颜色c,称某种颜色c支配以节点v为根的子树,当且仅当这种颜色c在以节点v为根的子树中是最多的。根据这个定义可以有多种颜色同时支配同一棵子树。对每个节点,分别求以其为根的子树的支配颜色的和。

题解:假如颜色少,可以树形dp,可惜搞不得,遂看题解。题解的意思是启发式合并,对于每个节点u,用个map<int,int>存每种颜色的出现次数。遍历一个节点u之前要先遍历完成他的所有子节点v,然后把v两两合并起来,每次把小的map合并到大的里面。统计哪个颜色是支配颜色的时候也不能遍历map,因为这样复杂度直接错了(在每个点都遍历一次颜色集合,复杂度肯定是错的)。具体的算法细节应该是这样:

假如某个点是叶子,那么构造一个map<int,int>,里面存{c,1},c是这个叶子的颜色,然后用个数字cnt记录支配颜色的次数为1,开个set(方便去重)记录支配颜色的颜色为c,再用个数字记录这些支配颜色的和为sum。

假如某个点只有1棵子树,那么支配颜色应该就是子树的支配颜色集合set中的字母或者这个点的颜色其中之一或者两者都是,理由如下。操作的时候就先让自己继承自己唯一子树的map、cnt、set、sum,然后把自己的颜色插进map,把因为插入操作修改的value的次数拿出来,有两种情况:value=cnt,这时这个自己的颜色就要尝试加入set,假如加入成功则sum+=c。或者value>cnt,这时清空set,再加入c,设sum=c。或者value<cnt,啥都没发生。

否则就至少有2棵子树,先继承siz最大的子树的map,因为这个算法的合并成本来源于遍历map,而不是子树高度之类的(虽然按照子树siz来启发式合并也是对的,反正最坏情况下每个节点的颜色就是他本身的编号,这样复杂度没什么区别),所以按照map的大小来启发式合并才是对的。然后仿照上面的过程,把其他子树的map中的颜色逐个插进去。最后记得插入自己的颜色。

复杂度为什么是对的呢?不要从颜色来考虑,颜色很难分析出什么,假如把map中因为颜色相同被合并到一起的节点们分开,那么就变成了最经典的启发式合并,在这个模型中每个节点最多会被拷贝logn次,因为每次这个节点被拷贝就意味着他要被合并去一棵比他大的子树中,所以新的树的大小至少是两倍。在这里一个节点对应一个颜色,把同种颜色合并在一起只会减少复杂度,不会增加复杂度。

收获:启发式合并经验+1,因为颜色种类数多,使用map是自然的。另外,要学会把颜色拆分回节点,这样子分析启发式合并的复杂度就非常容易。

写了一个,MLE26,看来上面的分析是对的,只是细节需要把握一下。

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

map<int, int> Map[100005];
int Cnt[100005];
set<int> Set[100005];
ll Sum[100005];

void InsertColor(int id, int col, int cnt) {
    Map[id][col] += cnt;
    int newcnt = Map[id][col];
    if(newcnt < Cnt[id])
        return;
    if(newcnt > Cnt[id]) {
        Cnt[id] = newcnt;
        Set[id].clear();
        Set[id].insert(col);
        Sum[id] = col;
        return;
    }
    Set[id].insert(col);
    Sum[id] += col;
    return;
}

void dfs(int u, int p) {
    int col = Col[u];
    if(G[u].size() == 1 && p != -1) {
        Map[u][col] = 1;
        Cnt[u] = 1;
        Set[u].insert(col);
        Sum[u] = col;
        return;
    }
    int MaxSizMap = -1, MaxSizMapV = -1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
        if(Map[v].size() > MaxSizMap) {
            MaxSizMap = Map[v].size();
            MaxSizMapV = v;
        }
    }
    swap(Map[u], Map[MaxSizMapV]);
    swap(Cnt[u], Cnt[MaxSizMapV]);
    swap(Set[u], Set[MaxSizMapV]);
    swap(Sum[u], Sum[MaxSizMapV]);

    InsertColor(u, col, 1);
    for(auto &v : G[u]) {
        if(v == p || v == MaxSizMapV)
            continue;
        for(auto &it : Map[v])
            InsertColor(u, it.first, it.second);
    }
    return;
}

void test_case() {
    int n;
    scanf("%d", &n);
    if(n == 1) {
        int c;
        scanf("%d", &c);
        printf("%d\n", c);
        return;
    }
    for(int i = 1; i <= n; ++i)
        scanf("%d", &Col[i]);
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, -1);
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", Sum[i], " \n"[i == n]);
}

仔细看看好像没有哪里浪费了空间。可能只有在退出dfs的时候析构子树了吧。仔细看看还有一点问题,就是这个Set根本就没有用,而且使用了algorithm的swap而不是自带的swap,题目又不关心支配颜色具体是哪些,可能领会错官方题解的意思了。

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

map<int, int> Map[100005];
int Cnt[100005];
ll Sum[100005];

void InsertColor(int id, int col, int cnt) {
    Map[id][col] += cnt;
    int newcnt = Map[id][col];
    if(newcnt < Cnt[id])
        return;
    if(newcnt > Cnt[id]) {
        Cnt[id] = newcnt;
        Sum[id] = col;
        return;
    }
    Sum[id] += col;
    return;
}

void dfs(int u, int p) {
    int col = Col[u];
    if(G[u].size() == 1 && p != -1) {
        Map[u][col] = 1;
        Cnt[u] = 1;
        Sum[u] = col;
        return;
    }
    int MaxSizMap = -1, MaxSizMapV = -1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
        if(Map[v].size() > MaxSizMap) {
            MaxSizMap = Map[v].size();
            MaxSizMapV = v;
        }
    }
    Map[u].swap(Map[MaxSizMapV]);
    swap(Cnt[u], Cnt[MaxSizMapV]);
    swap(Sum[u], Sum[MaxSizMapV]);

    InsertColor(u, col, 1);
    for(auto &v : G[u]) {
        if(v == p || v == MaxSizMapV)
            continue;
        for(auto &it : Map[v])
            InsertColor(u, it.first, it.second);
    }

    for(auto &v : G[u]) {
        if(v == p)
            continue;
        Map[v].clear();
    }
    return;
}

void test_case() {
    int n;
    scanf("%d", &n);
    if(n == 1) {
        int c;
        scanf("%d", &c);
        printf("%d\n", c);
        return;
    }
    for(int i = 1; i <= n; ++i)
        scanf("%d", &Col[i]);
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, -1);
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", Sum[i], " \n"[i == n]);
}

改成这样内存就少很多了,但是还是TLE了。莫非map的swap方法不是O(1)的?最多只能再从InsertColor函数中去掉一次二分了。

其实上面的各种MLE和TLE的bug是因为size()返回的是无符号类型,不能和-1比较,所以好好的siz的初始值就不要赋值为-1。

一种不需要swap的,直接用一个指针的写法。注意这种写法不能够析构被继承的子树的信息,因为父节点本身没有存信息,父节点只有一个指针。不过不被继承的子树是可以回收的。

int n;
int Col[100005];
vector<int> G[100005];

int Mapid[100005];
map<int, int> Map[100005];
int Cnt[100005];
ll Sum[100005];

void InsertColor(int id, int col, int cnt) {
    int &newcnt = Map[Mapid[id]][col];
    newcnt += cnt;
    if(newcnt < Cnt[id])
        return;
    if(newcnt > Cnt[id]) {
        Cnt[id] = newcnt;
        Sum[id] = col;
        return;
    }
    Sum[id] += col;
    return;
}

void dfs(int u, int p) {
    int col = Col[u];
    if(G[u].size() == 1 && p != -1) {
        Mapid[u] = u;
        Map[Mapid[u]][col] = 1;
        Cnt[u] = 1;
        Sum[u] = col;
        return;
    }
    int MaxSizMap = 0, MaxSizMapV = -1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
        if(Map[Mapid[v]].size() > MaxSizMap) {
            MaxSizMap = Map[Mapid[v]].size();
            MaxSizMapV = v;
        }
    }
    Mapid[u] = Mapid[MaxSizMapV];
    Cnt[u] = Cnt[MaxSizMapV];
    Sum[u] = Sum[MaxSizMapV];

    InsertColor(u, col, 1);
    for(auto &v : G[u]) {
        if(v == p || v == MaxSizMapV)
            continue;
        for(auto &it : Map[Mapid[v]])
            InsertColor(u, it.first, it.second);
    }
    return;
}

void test_case() {
    scanf("%d", &n);
    if(n == 1) {
        int c;
        scanf("%d", &c);
        printf("%d\n", c);
        return;
    }
    for(int i = 1; i <= n; ++i)
        scanf("%d", &Col[i]);
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, -1);
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", Sum[i], " \n"[i == n]);
}

带有swap的写法,实测map.swap和algorithm库的swap都没什么差别。不过用map进行赋值就是实打实的复制。这种写法可以回收所有子树的内存,但是没必要。

int Col[100005];
vector<int> G[100005];
 
map<int, int> Map[100005];
int Cnt[100005];
ll Sum[100005];
 
void InsertColor(int id, int col, int cnt) {
    Map[id][col] += cnt;
    int newcnt = Map[id][col];
    if(newcnt < Cnt[id])
        return;
    if(newcnt > Cnt[id]) {
        Cnt[id] = newcnt;
        Sum[id] = col;
        return;
    }
    Sum[id] += col;
    return;
}
 
void dfs(int u, int p) {
    int col = Col[u];
    if(G[u].size() == 1 && p != -1) {
        Map[u][col] = 1;
        Cnt[u] = 1;
        Sum[u] = col;
        return;
    }
    int MaxSizMap = 0, MaxSizMapV = -1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
        if((int)Map[v].size() > MaxSizMap) {
            MaxSizMap = Map[v].size();
            MaxSizMapV = v;
        }
    }
    Map[u].swap(Map[MaxSizMapV]);
    Cnt[u] = Cnt[MaxSizMapV];
    Sum[u] = Sum[MaxSizMapV];
 
    InsertColor(u, col, 1);
    for(auto &v : G[u]) {
        if(v == p || v == MaxSizMapV)
            continue;
        for(auto &it : Map[v])
            InsertColor(u, it.first, it.second);
    }
    return;
}

一些写法上的新经验:
1、size()返回的确实是unsigned类型,不要使用unsigned类型和int类型比较,假如一定要比较就强转成int。假如不手动强转,实测在GNU C++中默认会强转成unsigned进行比较。
2、假如特别喜欢用size()和int类型进行比较,就不要在int类型中使用负数值。
3、不要到处乱swap,比如上面的Sum,就是要输出答案的,这里就应该拷贝。
4、map的swap方法和algorithm的swap效率差不多,对map进行赋值确实会导致复制。
5、可以用int &newvalue=Map[key]获得引用,即使key原本不存在,也会正确创建并返回一个正确的引用。
6、与死循环导致TLE类似,死循环也有可能导致MLE。
7、也就是说,前面25个数据完全不需要继承信息,纯暴力过的。不仅有一个正数与“-1”比较的大bug,而且那些数组什么的都在和一个下标为-1的值进行交换,莫非与Sum[-1]进行交换不会影响答案?居然还信誓旦旦地说“算法几乎是对的”,把答案要输出的Sum都给弄没了还“对的”。
8、一个正确的算法,实现上也有很多需要考虑的细节。

posted @ 2020-03-11 04:17  KisekiPurin2019  阅读(163)  评论(0编辑  收藏  举报