2020牛客暑期多校训练营(第八场)

2020牛客暑期多校训练营(第八场)(2020.8.3)

不要问我为什么现在才写,因为懒。

全文口胡。

I、Interesting Computer Game

一开始以为是个匹配,结果旁边队冲了一发匈牙利被卡掉了。然后写不出来。

结果打完看题解发现确实是个图论。把所有出现过的数字先离散化,然后若两数分在一组则中间连一条边。那么对于每一条边都只能选其中一个端点。

考虑树形结构,则除了根节点以外其它节点都可以选到。而若在树形结构上多加一条边,则所有的节点都可以选了。所以最后只要弄一下图里连通块的数量,按照边数统计每个连通块的贡献就好了。

对于边的处理单独去数其实比较麻烦。可以根据握手定理统计每个节点的度数,其实就是存该节点的vector的size。自环需要特判一下,因为自环对自己的贡献为2,并且不会在其它节点被统计到。

后记:很神奇的是我在复制的时候发现自己之前补的AC代码居然没特判自环。出题人背锅。现在这份判了,应该没什么大问题。

#include 
using namespace std;
typedef long long ll;
const int MAXN = 3e5 + 10;
vector <int> G[MAXN];
pair <int, int> a[MAXN];
bool vis[MAXN];
int v = 0, e = 0;
void dfs(int u)
{
    v++; e += G[u].size();
    vis[u] = 1;
    for (auto &i : G[u])
    {
        if (!vis[i]) dfs(i);
        if (u == i) ++e;
    }
}
int main()
{
    int t; cin >> t;
    for (int p = 1; p <= t; ++p)
    {
        int n; cin >> n;
        vector <int> idx;
        for (int i = 1; i <= n; ++i)
        {
            int x, y; scanf("%d%d", &x, &y);
            a[i] = {x, y}; idx.push_back(x), idx.push_back(y);
        }
        sort(idx.begin(), idx.end());
        idx.erase(unique(idx.begin(), idx.end()), idx.end());
        for (int i = 0; i < idx.size(); ++i) G[i].clear();
        for (int i = 1; i <= n; ++i)
        {
            int u = lower_bound(idx.begin(), idx.end(), a[i].first) - idx.begin();
            int v = lower_bound(idx.begin(), idx.end(), a[i].second) - idx.begin();
            G[u].push_back(v), G[v].push_back(u);
        }
        memset(vis, 0, sizeof(vis)); int ans = 0;
        for (int i = 0; i < idx.size(); ++i)
            if (!vis[i]) v = 0, e = 0, dfs(i), ans += (e / 2 == v - 1? v - 1: v);
        printf("Case #%d: %d\n", p, ans);
    }
    return 0;
}

K、Kabaleo Lite

这场唯一出的一道题。% 璇大师 %

似乎有很多 O ( n l o g n ) O(nlogn) O(nlogn)的方法。不过璇大师的代码是 O ( n ) O(n) O(n)的。

首先发现选的个数一定是 b [ 1 ] b[1] b[1]

之后想象出一个虚拟数组,个数为 b [ 1 ] b[1] b[1],初始化每个值都为 a [ 1 ] a[1] a[1]。之后从前往后遍历。假设当前遍历到 i i i,那么如果记当前元素能选的个数最多为 c [ i ] = m i n j = 1 i { b [ j ] } c[i]=min_{j=1}^i\{b[j]\} c[i]=minj=1i{b[j]}个,更新方法就是把虚拟数组的前 c [ i ] c[i] c[i]个覆盖为 s u m [ i ] sum[i] sum[i]。这个其实很好理解。

之后观察性质。首先,我们发现 c [ i ] c[i] c[i]一定是递减的。其次,我们发现若当前元素需要更新数组,那么当前元素的前缀和一定会比之前最大的前缀和更大。

而利用 c [ i ] c[i] c[i]是递减的,就意味着后面更新的元素个数一定小于前面更新的元素个数,所以在虚拟数组中所对应的更新的那一段区间的数字一定都是相同的,而且都等于它之前第一个比它大的数。

通过这两个性质就可以很方便地更新数值了。所以连数组都不需要开,记两个变量就好。具体见代码。

这题还有一个坑点就是爆了ll。赛场__int128现学现卖。

后来发现我一开始想的那个用优先队列的贪心方法其实是对的,只是因为爆了ll所以哇了。

不过我的方法实在太烂,这里只介绍璇式方法。并且我个人觉得这个方法应该可以继续拓展解决一些奇奇怪怪的问题,不过暂时还没想到(x

#include 
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
ll a[MAXN], b[MAXN];
void print(__int128_t t)
{
    bool np = 0; if (t < 0) np = 1, t *= -1;
    vector <int> dig;
    while (t) dig.push_back(t % 10), t /= 10;
    if (np) printf("-");
    for (auto i = dig.rbegin(); i != dig.rend(); ++i) printf("%d", *i);
}
int main()
{
    int t; cin >> t;
    for (int p = 1; p <= t; ++p)
    {
        int n; cin >> n;
        for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]);
        for (int i = 1; i <= n; ++i) scanf("%lld", &b[i]);
        __int128_t sum = a[1] * b[1], all = a[1];//all:前缀和
        ll prev = a[1], mini = b[1];//prev:当前最大的前缀和 mini:当前最小的b[i]
        for (int i = 2; i <= n; ++i)
        {
            all += a[i];
            mini = min(mini, b[i]);
            if (all > prev) sum += (all - prev) * mini, prev = all;
        }
        printf("Case #%d: %d ", p, b[1]);
        print(sum); cout << endl;
    }
    return 0;
}

赛后总结:

《论罚坐》

你可能感兴趣的:(2020暑期多校)