2020牛客寒假算法基础集训营2

题目链接:https://ac.nowcoder.com/acm/contest/3003
题解连接:https://ac.nowcoder.com/discuss/364961

A - 做游戏

不能叫题。

B - 排数字

题意:给一个数字串,任意重排它。求最多有多少个子串完全等于"616"。

显然应该尽可能重复利用6。假如把这道题推广到更长的情况应该有点意思,就要把KMP算的那个“最长的真前缀和真后缀完全重叠部分”重复利用。

G - 判正误

题意:给一些很大的数字 (a,b,c,d,e,f,g) ,求验证是否 (a^d+b^e+c^f=g)

题解:取很多个p去验证同余式正确即可,注意处理负数的情况,但是准确的证明要怎么做呢?中国剩余定理可以处理这种情况吗?

总结:原来快速幂算法算负数的非负数次幂也是正确的。

D - 数三角

n比较小,才500,所以暴力即可。需要注意的是这里的虽然没有重点,但是这里的三角形不考虑退化的情况(退化的三角形其实确实应该是钝角三角形)。

所以用叉积求出面积(的两倍)就知道是否退化。注意 ^ 运算符的优先级特别低。

其实用 (a^2+b^2<c^2) 由余弦定理就是 (cosC<0)

假如n变大一点的话,是否可以枚举钝角三角形的长边作为直径然后统计有多少点在圆内呢,貌似只想到一种容易退化的就是求出这个圆的圆心(废话就是直径中点,然后加减半径求出一个范围,只验证在这个范围内的点。在数据随机的条件下可能表现会好一点?)由于钝角三角形的长边是唯一的所以也不需要去重,但是由于这题的恶心条件需要去掉共线的情况。

struct Point {
    ll x, y;
    Point() {}
    Point(ll x, ll y): x(x), y(y) {}
    friend Point operator-(const Point &a, const Point &b) {
        return Point(a.x - b.x, a.y - b.y);
    }
    friend ll operator*(const Point &a, const Point &b) {
        return a.x * b.x + a.y * b.y;
    }
    friend ll operator^(const Point &a, const Point &b) {
        return a.x * b.y - a.y * b.x;
    }
} p[505];

void test_case() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        p[i] = Point(x, y);
    }
    int cnt = 0;
    for(int i = 1; i <= n; ++i) {
        Point A = p[i];
        for(int j = i + 1; j <= n; ++j) {
            Point B = p[j];
            for(int k = j + 1; k <= n; ++k) {
                Point C = p[k];
                if(((A - C) ^ (B - C)) == 0)
                    continue;
                if((A - C) * (B - C) < 0 || (A - B) * (C - B) < 0 || (B - A) * (C - A) < 0)
                    ++cnt;
            }
        }
    }
    printf("%d
", cnt);
}

E - 做计数

题意:给一个 (n(1leq n leq 4cdot 10^7)) 求有多少对正整数有序对 ((i,j,k)) 满足 (ijleq n)(sqrt{i}+sqrt{j}=sqrt{k})

题解:两边平方得: (i+j+2sqrt{ij}=k) 由于 (k) 没有被限制所以只需要 (ij) 是某个完全平方数即可。所以可以枚举 (n) 以内的完全平方数,然后求出其因子数即可。

int calc(int x) {
    int cnt = 0;
    for(int i = 1; i * i <= x; ++i) {
        if(x % i == 0) {
            ++cnt;
            if(i * i != x)
                ++cnt;
        }
    }
    return cnt;
}

void test_case() {
    int n;
    scanf("%d", &n);
    ll cnt = 0;
    for(int i = 1; i * i <= n; ++i)
        cnt += calc(i * i);
    printf("%lld
", cnt);
}

*F - 拿物品

题意:有 (n(1leq nleq 2cdot 10^5)) 个物品,其中第 (i) 个物品的属性是 ({a_i,b_i}) ,A和B轮流拿物品,A先拿。A的总分是他拿的所有物品的 (a_i) 之和,B同理。两人都想最大化自己的分减去对方的分的差。求分别拿了哪些物品?

题解:用微扰法应该才是对的,有两个物品 (i,j) ,假如A拿 (i) B拿 (j) ,那么分差就是 (a_i-b_j) ,假如交换,则是 (a_j-b_i) 轮到A行动时,当 (a_i-b_j>a_j-b_i) 则取 (i) ,移项得 (a_i+b_i>a_j+b_j) 。轮到B行动时,当 (b_i-a_j>b_j-a_i) 则取 (i) ,移项得 (a_i+b_i>a_j+b_j) ,所以应该按和排序,然后轮流取最大值。

pii p[200005];

void test_case() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        p[i] = {x, i};
    }
    for(int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        p[i].first += x;
    }
    sort(p + 1, p + 1 + n, greater<pii>());
    printf("%d", p[1].second);
    for(int i = 3; i <= n; i += 2)
        printf(" %d", p[i].second);
    puts("");
    printf("%d", p[2].second);
    for(int i = 4; i <= n; i += 2)
        printf(" %d", p[i].second);
    puts("");
}

注意:排序之后轮流取最大值,肯定是要把n分给先拿的人的啊!搞什么啊,下次乖乖从大到小排!

C - 算概率

题意:有 (n(1leq nleq 2000)) 道题,第 (i) 个问题的正确率是 (p_i) ,求最后正确 (k(0leq kleq n)) 道题的概率。

题解:数据小直接dp。设 (dp[i][j]) 表示前 (i) 道题正确 (j) 道的概率。

int p[2005], q[2005];
ll dp[2005][2005];

void test_case() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &p[i]);
        q[i] = 1 - p[i];
        if(q[i] < 0)
            q[i] += MOD;
    }
    dp[0][0] = 1;
    for(int i = 1; i <= n; ++i) {
        dp[i][0] = dp[i - 1][0] * q[i] % MOD;
        for(int j = 1; j <= i; ++j) {
            dp[i][j] = (dp[i - 1][j] * q[i] + dp[i - 1][j - 1] * p[i]) % MOD;
        }
    }
    for(int j = 0; j <= n; ++j)
        printf("%lld%c", dp[n][j], " 
"[j == n]);
}

*H - 施魔法

题意:给 (n(1leq n leq 3cdot10^5)) 个数 (a_i(0leq a_i leq 10^9)) ,划分为若干个至少有 (k(1leq k leq n)) 个元素的集合。代价为所有集合的极差的和。最小化代价。

题解:由于最终影响的是极差,所以选定最大最小元素之后肯定是把中间的全部用完最好,所以一定是先排序。然后显然有个naive的dp方式: (dp[i]) 表示把前 (i) 个元素划分为若干个集合的最小代价,则有转移 (dp[i]=minlimits_{j=0}^{i-k+1}(dp[j]+a[i]-a[j+1])) 所以 (dp[i]=a[i]+minlimits_{j=0}^{i-k+1}(dp[j]-a[j+1])) 连单调队列都不用。

int a[300005];
ll dp[300005];
ll mi[300005];

void test_case() {
    int n, k;
    scanf("%d%d", &n, &k);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    sort(a + 1, a + 1 + n);
    dp[0] = 0;
    mi[0] = dp[0] - a[1];
    for(int i = 1; i <= n; ++i) {
        dp[i] = a[i] + ((i >= k) ? mi[i - k] : LINF);
        mi[i] = min(mi[i - 1], dp[i] - a[i + 1]);
    }
    printf("%lld
", dp[n]);
}

总结:注意看清楚下标。

*I - 建通道

题意:有 $n(1leq n leq 2cdot 10^5) $ 的完全图,边的权值为两点的权值的异或的lowbit,求mst。

错误解法:既然是求mst,考虑像Kruskal算法一样给边排序逐个加入?先考虑lowbit为0的,也就是全等的点全部连上。然后就应该是个位不同的点全部连上。考虑在并查集合并了之后,整个连通块带上了原本的两个连通块的特征,所以应该可以分别考虑32个位,对于某位,连通块是只能为0还是只能为1还是01都行,这个可以用两个整数(bool向量)来维护。连接全等(其实全等的连不连不是一样的吗)逐次从低位合并就可以,某位假如有“01都行”块,则这一位不贡献,否则这一位贡献它本身。易知结果不超32位整数。

题解:上面的解法错在误会了lowbit的含义,事实上方法是类似的,先去重,然后从低位开始依次枚举30位。设当前2的幂为x,把所有元素按x这一位的0和1分类。分到同一类里面的元素不存在代价为x的边,两类任意一对元素之间都有代价为x的边。然后对于某个当前位为0的假如和当前位为1的不连通,那么此时花费x的代价来连接他们(由Kruskal算法知)是正确的。

而且应该进行一次连接之后整个图就已经完全连通了,所以答案就是最小的既有0又有1的二进制位 (2^k(n-1)) ,其中 (n) 是去重后的结果。

不过本质上复杂度差不多。

int a[200005];

struct DisjointSetUnion {
    static const int MAXN = 200000;
    int n, fa[MAXN + 5], rnk[MAXN + 5];

    void Init(int _n) {
        n = _n;
        for(int i = 1; i <= n; i++) {
            fa[i] = i;
            rnk[i] = 1;
        }
    }

    int Find(int u) {
        int r = fa[u];
        while(fa[r] != r)
            r = fa[r];
        int t;
        while(fa[u] != r) {
            t = fa[u];
            fa[u] = r;
            u = t;
        }
        return r;
    }

    bool Merge(int u, int v) {
        u = Find(u), v = Find(v);
        if(u == v)
            return false;
        else {
            if(rnk[u] < rnk[v])
                swap(u, v);
            fa[v] = u;
            rnk[u] += rnk[v];
            return true;
        }
    }
} dsu;

int id0[200005], top0;
int id1[200005], top1;

void test_case() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    sort(a + 1, a + 1 + n);
    n = unique(a + 1, a + 1 + n) - (a + 1);
    dsu.Init(n);
    ll sum = 0;
    for(int b = 0; b < 30; ++b) {
        int bitmask = 1 << b;
        top0 = 0, top1 = 0;
        for(int i = 1; i <= n; ++i) {
            if((a[i]&bitmask) == 0)
                id0[++top0] = i;
            else
                id1[++top1] = i;
        }
        if(top1)
            for(int x = 1; x <= top0; ++x) {
                if(dsu.Merge(id1[1], id0[x]))
                    sum += bitmask;
            }
        if(top0)
            for(int x = 1; x <= top1; ++x) {
                if(dsu.Merge(id0[1], id1[x]))
                    sum += bitmask;
            }
        //cout << "bitmask=" << bitmask << " sum=" << sum << endl;
    }
    printf("%lld
", sum);
}

反思:在for里面访问数组时,当前数组的siz已经被for限制,但是假如用到另一个数组的siz,要注意特判。

*J - 求函数

题意:给一堆一次函数 (f_i(x)=k_ix+b_i) ,维护两种操作:

1、((i,k,b)) 把函数 (f_i(x)) 设为 (f_i(x)=kx+b)
2、((l,r)) 求函数 (f_r(f_{r-1}(...f_{l+1}(f_l(1)))))

题解:其实一看就在想是否满足结合律,由复合函数的运算规则 (fcdot (g cdot h)=(fcdot g) cdot h) 所以满足结合律。(其实就是加个括号区分一下内外,反正都是 (f(g(h(x))))

思考1:能不能改求 ((l,r,x)) 求函数 (f_r(f_{r-1}(...f_{l+1}(f_l(x))))) ?事实上一次函数之间复合还是一次函数,所以线段树可以保存一段区间的复合函数的复合结果,证明: (f_1(x)=k_1x+b_1,f_2(x)=k_1x+b_1)(f_2(f_1(x))=k_2(k_1x+b_1)+b_2)(f_2(f_1(x))=k_2k_1x+k_2b_1+b_2) 也是一次函数,由归纳法知得证。(不过二次函数复合的结果就应该是四次函数(不考虑退化))事实上不能只保存函数运算的结果,因为k和b对复合的贡献要分开。

int n, m;
int K[200005], B[200005];

struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
    static const int MAXN = 200000;
    ll k[(MAXN << 2) + 5], b[(MAXN << 2) + 5];

    void PushUp(int o) {
        k[o] = k[rs] * k[ls];
        b[o] = k[rs] * b[ls] + b[rs];
        if(k[o] >= MOD)
            k[o] %= MOD;
        if(b[o] >= MOD)
            b[o] %= MOD;
    }

    void Build(int o, int l, int r) {
        if(l == r) {
            k[o] = K[l];
            b[o] = B[l];
        } else {
            int m = l + r >> 1;
            Build(ls, l, m);
            Build(rs, m + 1, r);
            PushUp(o);
        }
    }

    void Update(int o, int l, int r, int p, int K, int B) {
        if(l == r) {
            k[o] = K;
            b[o] = B;
            return;
        } else {
            int m = l + r >> 1;
            if(p <= m)
                Update(ls, l, m, p, K, B);
            if(p >= m + 1)
                Update(rs, m + 1, r, p, K, B);
            PushUp(o);
        }
    }

    pll Query(int o, int l, int r, int ql, int qr) {
        if(ql <= l && r <= qr) {
            return {k[o], b[o]};
        } else {
            int m = l + r >> 1;
            pll res = {1, 0};
            if(ql <= m)
                res = Query(ls, l, m, ql, qr);
            if(qr >= m + 1) {
                pll tmp = Query(rs, m + 1, r, ql, qr);
                res.first *= tmp.first;
                res.second *= tmp.first;
                res.second += tmp.second;
                if(res.first >= MOD)
                    res.first %= MOD;
                if(res.second >= MOD)
                    res.second %= MOD;
            }
            return res;
        }
    }
#undef ls
#undef rs
} st;

void test_case() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &K[i]);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &B[i]);
    st.Build(1, 1, n);
    while(m--) {
        int op;
        scanf("%d", &op);
        if(op == 1) {
            int i, k, b;
            scanf("%d%d%d", &i, &k, &b);
            st.Update(1, 1, n, i, k, b);
        } else {
            int l, r;
            scanf("%d%d", &l, &r);
            pll res = st.Query(1, 1, n, l, r);
            ll ans = res.first + res.second;
            if(ans >= MOD)
                ans %= MOD;
            printf("%lld
", ans);
        }
    }
}

思考2:能不能区间修改?推出三个相同的函数复合的结果:

(f(x)=kx+b)
(f(f(x))=k^2x+kb+b)
(f(f(f(x)))=k^3x+k^2b+kb+b)

所以 (n) 次复合的结果就应该是 (K=k^n,B=bsumlimits_{i=0}^{t-1}k^t=bfrac{k^n-1}{k-1})

原文地址:https://www.cnblogs.com/KisekiPurin2019/p/12269898.html