比赛题目训练系列07 (2020ICPC 江西省赛)

比赛题目训练系列08 (2020ICPC 江西省赛)

训练网址

A. A Simple Math Problem

  • 题意直接放图片吧,题意很简单。
    在这里插入图片描述
  • 莫比乌斯反演,一个很重要的作用是交换枚举次序。
  • 先放一个大雪菜在莫比乌斯反演部分推到过的一个公式,防止遗忘
    s ( n ) = ∑ d ∣ n μ ( d ) = { 1 , n = 1 0 , n > 1 . s(n) = \sum\limits_{d|n}\mu(d) = \begin{cases}1,n=1\\0, n>1 \end{cases}. s(n)=dnμ(d)={1,n=10,n>1.
  • 推导是这样的
    ∑ j = 1 n ∑ i = j n F ( j ) [ g c d ( i , j ) = = 1 ] = ∑ j = 1 n F ( j ) ∑ i = j n ∑ d ∣ ( i , j ) μ ( d ) = ∑ j = 1 n ∑ i = j n ∑ d ∣ ( i , j ) F ( j ) μ ( d ) \sum\limits_{j=1}^n\sum_{i=j}^n F(j)[gcd(i, j) == 1] \\ =\sum\limits_{j=1}^nF(j)\sum\limits_{i = j}^{n}\sum\limits_{d|(i,j)}\mu(d) \\ =\sum\limits_{j=1}^{n}\sum\limits_{i=j}^n\sum\limits_{d|(i,j)}F(j)\mu(d) j=1ni=jnF(j)[gcd(i,j)==1]=j=1nF(j)i=jnd(i,j)μ(d)=j=1ni=jnd(i,j)F(j)μ(d)
  • 我们尝试交换枚举次序,枚举 d d d,用于 d d d 的含义是整除 g c d ( i , j ) gcd(i,j) gcd(i,j),因此d一定整除i,且d一定整除j. 因此,我们只需要枚举d的倍数即可
    原 式 = ∑ d = 1 n μ ( d ) ∑ j ′ = 1 ⌊ n / d ⌋ F ( d ∗ j ′ ) ∑ i ′ = j ′ ⌊ n / d ⌋ 1 = ∑ d = 1 n μ ( d ) ∑ j ′ = 1 ⌊ n / d ⌋ F ( d ∗ j ′ ) ( ⌊ n / d ⌋ − j ′ + 1 ) . 原式 = \sum\limits_{d=1}^n\mu(d)\sum\limits_{j'=1}^{\lfloor n/d \rfloor}F(d*j') \sum\limits_{i' = j'}^{\lfloor n/d \rfloor} 1 \\ = \sum\limits_{d=1}^n\mu(d)\sum\limits_{j'=1}^{\lfloor n/d \rfloor}F(d*j') (\lfloor n/d \rfloor - j' + 1). =d=1nμ(d)j=1n/dF(dj)i=jn/d1=d=1nμ(d)j=1n/dF(dj)(n/dj+1).
  • 预处理出来莫比乌斯函数、F函数就可以了。时间复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn).
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long ll;

const int maxn = 100010;
int prime[maxn], cnt, mu[maxn], F[maxn];
bool st[maxn];

void sieve(int N)
{
    mu[1] = 1;
    for(int i = 2; i <= N; i++){
        if(!st[i]) prime[cnt++] = i, mu[i] = -1;
        for(int j = 0; prime[j] <= N / i; j++){
            st[i * prime[j]] = true;
            if(i % prime[j] == 0) break;
            mu[i * prime[j]] = -mu[i];
        }
    }
    for(int i = 0; i <= N; i++){
        int x = i;
        while(x){
            F[i] += x % 10;
            x /= 10;
        }
    }
}
int main()
{
    sieve(maxn - 1);
    int N;
    scanf("%d", &N);
    ll ans = 0;
    for(int d = 1; d <= N; d++){
        if(!mu[d]) continue;
        ll res = 0;
        for(int j = 1; j <= N / d; j++){
            res += (ll)F[j * d] * (N / d - j + 1LL);
        }
        ans += mu[d] * res;
    }
    printf("%lld\n", ans);
    return 0;
}

B. Apple

  • 签到
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
ll N, M;
int main()
{
    int T;
    scanf("%d", &T);
    while(T--){
        scanf("%lld%lld\n", &N, &M);
        if(M * (M + 1) / 2 <= N) printf("possible\n");
        else printf("impossible\n");
    }
    return 0;
}

C.

D.

E. Color Sequence

  • 给定一个序列,问有多少个连续子序列,满足这样的要求:序列中每个数出现的次数都是偶数。序列中的数字范围是 0 ≤ a i ≤ 20. 0\le a_i \le 20. 0ai20.
  • 我们发现了一些性质:
    • 我们看到只需要判断奇偶性,因此可以想到对2取模。
    • 因为只有21个数字,因此我们可以用二进制来维护这个子序列的状态。
    • 两个相邻的子序列的状态之和就是两个二进制数字的异或。
    • 一个连续子序列是否满足要求,等价于两个前缀和 S r = = S l − 1 S_r == S_{l - 1} Sr==Sl1.
  • 因此,我们很自然地想到一个方法,统计所有前缀和,用 unordered_map 把这些东西统计起来,设每一个前缀和的数量是 c n t cnt cnt,则答案就加上 C c n t 2 C_{cnt}^{2} Ccnt2.
#include<iostream>
#include<cstring>
#include<algorithm>
#include<bitset>
#include<unordered_map>
using namespace std;
const int maxw = 25;
typedef bitset<maxw> bt;
unordered_map<bt, int> mp;
int N;
typedef long long ll;
int main()
{
    scanf("%d", &N);
    bt now;
    now.reset();
    mp[now]++;
    for(int i = 1; i <= N; i++)
    {
        int x;
        scanf("%d", &x);
        now[x] = now[x] ^ 1;
        mp[now]++;
    }
    ll ans = 0;
    for(auto p : mp){
        ll cnt = p.second;
        ans += (cnt - 1) * cnt / 2;
    }
    cout << ans << endl;
    return 0;
}

F.

G. Mathematical Practice

  • 题意:从一个大小为 n n n 的集合中,有顺序地取 m m m 次子集,求这些子集两两没有交集的方案数。
  • 观察数据猜测,答案是 ( m + 1 ) n (m+1)^n (m+1)n. 因为 n n n m m m 都达到了1e9,因此答案很可能就是某个数的幂,而不太可能出现和阶乘相关的东西(因为过的人数太多了)。下面来证明一下这个结论:
    在这里插入图片描述
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const ll mod = 998244353;
ll mod_pow(ll x, ll n)
{
    ll res = 1;
    while(n){
        if(n & 1) res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}
int main()
{
    ll N, M;
    cin >> N >> M;
    cout << mod_pow(M + 1, N) << endl;
    return 0;
}

H. Sequence

  • 动态维护一个序列(带修改),找到最小值为 a x a_x ax 的连续子序列的数量。
  • 核心内容就是如何找到左边第一个比它小的数和右边第一个比它小的数。
  • 以找到右边第一个比它小的数举例:

法一:

  • 可以二分搜索,在 [x, N] 找到最小值是否小于x(线段树维护)。我们对查询的区间长度二分 (lb = 0, ub = N - x + 1). 如果当前长度找到比它小的数,就缩短查询区间长度;如果当前长度找到比它小的数,就扩大区间长度。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010;
int w[maxn];
typedef long long ll;
struct node
{
    int l, r, mmin;
}tr[maxn * 4];
int N, M;
void pushup(int u)
{
    tr[u].mmin = min(tr[2 * u].mmin, tr[2 * u + 1].mmin);
}
void build(int u, int l, int r)
{
    tr[u] = {l, r};
    if(l == r){
        tr[u].mmin = w[l];
    }
    else{
        int mid = (l + r) / 2;
        build(2 * u, l, mid), build(2 * u +1, mid +1, r);
        pushup(u);
    }
}
int query(int u, int L, int R)
{
    if(L <= tr[u].l && tr[u].r <= R){
        return tr[u].mmin;
    }
    int res = (1LL << 30) - 1;
    int mid = (tr[u].l + tr[u].r) / 2;
    if(L <= mid) res = query(2 * u, L, R);
    if(R > mid) res = min(res, query(2 * u + 1, L, R));
    return res;
}
void update(int u, int x, int y)
{
    if(tr[u].l == x && tr[u].r == x){
        tr[u].mmin = y;
        return;
    }
    int mid = (tr[u].l + tr[u].r) / 2;
    if(x <= mid) update(2 * u, x, y);
    else update(2 * u +1, x, y);
    pushup(u);
}
int main()
{
    scanf("%d%d", &N, &M);
    for(int i = 1; i <= N; i++){
        scanf("%d", &w[i]);
    }
    build(1, 1, N);
    while(M--){
        int opt, x, y;
        scanf("%d", &opt);
        if(opt == 1){
            scanf("%d%d", &x, &y);
            w[x] = y;
            update(1, x, y);
        }
        else{
            scanf("%d", &x);
            int lb = 1, ub = N - x + 1;
            while(ub > lb){
                int mid = (lb + ub + 1) / 2;
                if(query(1, x, x + mid - 1) >= w[x]) lb = mid;
                else ub = mid - 1;
            }
            int ans1 = lb;
            lb = 1, ub = x;
            while(ub > lb){
                int mid = (lb + ub + 1) / 2;
                if(query(1, x - mid + 1, x) >= w[x]) lb = mid;
                else ub = mid - 1;
            }
            int ans2 = lb;
            //printf("*** %d %d\n", ans1, ans2);
            printf("%lld\n", (ll)ans1 * (ll)ans2);
        }
    }
    return 0;
}

法二:

具体做法:只考虑求 a x a_x ax左边的值,右边类似
1.因为要尽量离 a x a_x ax近,那么贪心的检查右子树中是否含 x x x。有 并且 区间最小值 a x a_x ax 的话. 那么右边可能存在答案(1.2为什么说可能,因为可能存在 唯一 一个 v a l < a x val < a_x val<ax,其位置 > x > x >x. 这样答案就不存在,但是我们还是会往右子树走.)
2.如果右子树不符合要求,自然就往左子树看。若左子树最小值 < a x < a_x <ax. 那就往左子树搜索(这个时候答案是一定存在的).
根据:只要往左走,那么答案就一定存在。考虑其生成树的样子。要么是一直往右走。要么只能往左拐一下就直接找到答案。那么单次查询最差也就两条链。即 O ( 2 log ⁡ n ) O(2\log n) O(2logn) 的复杂度.
————————————————
版权声明:本文为CSDN博主「Implicit_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35577488/article/details/109923150

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010;
int w[maxn];
typedef long long ll;
struct node
{
    int l, r, mmin;
}tr[maxn * 4];
int N, M;
void pushup(int u)
{
    tr[u].mmin = min(tr[2 * u].mmin, tr[2 * u + 1].mmin);
}
void build(int u, int l, int r)
{
    tr[u] = {l, r};
    if(l == r){
        tr[u].mmin = w[l];
    }
    else{
        int mid = (l + r) / 2;
        build(2 * u, l, mid), build(2 * u +1, mid +1, r);
        pushup(u);
    }
}
void update(int u, int x, int y)
{
    if(tr[u].l == x && tr[u].r == x){
        tr[u].mmin = y;
        return;
    }
    int mid = (tr[u].l + tr[u].r) / 2;
    if(x <= mid) update(2 * u, x, y);
    else update(2 * u +1, x, y);
    pushup(u);
}
int query1(int u, int L, int R)
{
    if(tr[u].l == tr[u].r){
        if(tr[u].mmin >= w[R]) return 0;
        return tr[u].l;
    }
    int mid = (tr[u].l + tr[u].r) / 2;
    if(R > mid && tr[2 * u + 1].mmin < w[R]){
        int res = query1(2 * u + 1, L, R);
        if(res != 0) return res;
    }
    if(L <= mid && tr[2 * u].mmin < w[R]){
        return query1(2 * u, L, R);
    }
    return 0;
}
int query2(int u, int L, int R)
{
    if(tr[u].l == tr[u].r){
        if(tr[u].mmin >= w[L]) return N + 1;
        return tr[u].l;
    }
    int mid = (tr[u].l + tr[u].r) / 2;
    if(L <= mid && tr[2 * u].mmin < w[L]){
        int res = query2(2 * u, L, R);
        if(res != N + 1) return res;
    }
    if(R > mid && tr[2 * u + 1].mmin < w[L]){
        return query2(2 * u + 1, L, R);
    }
    return N + 1;
}
int main()
{
    scanf("%d%d", &N, &M);
    for(int i = 1; i <= N; i++){
        scanf("%d", &w[i]);
    }
    build(1, 1, N);
    while(M--){
        int opt, x, y;
        scanf("%d", &opt);
        if(opt == 1){
            scanf("%d%d", &x, &y);
            w[x] = y;
            update(1, x, y);
        }
        else{
            scanf("%d", &x);
            ll ans1 = x - query1(1, 1, x), ans2 = query2(1, x, N) - x;
            printf("%lld\n", ans1 * ans2);
        }
    }
    return 0;
}

I. Simple Math Problem

  • 这个题的题意再补充一句话,就是行和列的下标都从0开始,输出的话要输出十进制的答案
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
ll n, x, y;
int main()
{
    scanf("%lld%lld%lld", &n, &x, &y);
    if(x + y < n){
        ll ans = (x + y) * (x + y + 1) / 2 + x;
        printf("%lld\n", ans);
    }
    else{
        ll ans = n * n - (2 * n - x - y - 2) * (2 *n - x - y - 1) / 2 - n + x;
        printf("%lld\n", ans);
    }
    return 0;
}

J. Split Game

  • 题意:两个人在玩如下游戏。准备一张分成 w ∗ h w*h wh 的格子的长方形纸张,两人轮流切割纸张。要沿着格子的边界切割,水平或者垂直地将纸张切成两部分。切割了n次之后就得到了n+1张纸,每次都选择切得的某一张纸再进行切割。首先切出只有一个格子的纸张( 1 ∗ 1 1*1 11的各自组成的纸张)的一方失败。当双方都采取最优策略时,先手是必胜,还是必败?
  • 我们这个题,可以看作把一堆石子按照某些规则分成两堆,然后把这两堆的石子对应的SG的异或值推入集合S,然后把当前这个状态的SG值赋值为 m e s h { S } mesh\{S\} mesh{S}.
  • 注意这个题和白书上的那个模板题不太一样。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_set>
using namespace std;
const int maxn = 160;
int f[maxn][maxn];

int sg(int n, int m)
{
    if(f[n][m] != -1) return f[n][m];
    unordered_set<int> S;
    for(int i = 1; n - i >= 1; i++) {
        //选手会想尽办法不能进入(1,1)的状态,这个和取石子的题不一样。取石子是选手想办法走到(0,0)的状态。
        //如果无法移动,那么就意味着当前节点就是必败态。即此时S是空集。
        if(i == 1 && m == 1 || n - i == 1 && m == 1) continue;
        S.insert(sg(i, m) ^ sg(n - i, m));
    }
    for(int i = 1; m - i >= 1; i++) {
        if(i == 1 && n == 1 || m - i == 1 && n == 1) continue;
        S.insert(sg(n, i) ^ sg(n, m - i));
    }
    for(int i = 0; ; i++){
        if(!S.count(i)) return f[n][m] = i;
    }
}
int main()
{
    int N, M;
    memset(f, -1, sizeof f);
    while(cin >> N >> M){
        if(sg(N, M) == 0) printf("Bob\n");
        else printf("Alice\n");
    }
    return 0;
}

K. Travel Expense

  • 题意:有 n 座城市,m 条双向道路,一条道路要走一天,每条道路的费用是 items ^ days ( items 是所携带的物品数量, days 是第几天),给你一个出发城市 S ,目的地城市 T ,预算 B ,问最多能携带多少物品。
  • 中间有个地方我用的二分搜索,用了浮点数担心会有较大误差但是实际上还是过了那个题所以到底是浮点数误差不会造成影响还是数据需要加强呢?
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int N, M;
const int maxn = 110, INF = 0x3f3f3f3f;
int G[maxn][maxn];
typedef long long ll;
ll qpow(ll x, ll n)
{
    ll res = 1;
    while(n){
        if(n & 1) res *= x;
        x *= x;
        n >>= 1;
    }
    return res;
}
bool check(ll q, ll d, ll B)
{
    if(q == 1) return d <= B;
    return (pow(q, d + 1) - q) / (q - 1) <= B;
    //return (qpow(q, d + 1) - q) / (q - 1) <= B;

}
int main()
{
    scanf("%d%d", &N, &M);
    memset(G, 0x3f, sizeof G);
    while(M--){
        int a, b;
        scanf("%d%d", &a, &b);
        G[a][b] = G[b][a] = 1;
    }
    for(int k = 1; k <= N; k++){
        for(int i = 1; i <= N; i++){
            for(int j = 1; j <= N; j++){
                G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
            }
        }
    }
    int Q;
    scanf("%d", &Q);
    while(Q--){
        int S, T;
        ll B;
        scanf("%d%d%lld", &S, &T, &B);
        ll min_d = G[S][T];
        ll l = 0, r = B + 1;
        while(r > l){
            ll mid = (l + r + 1) / 2;
            if(check(mid, min_d, B)) l = mid;
            else r = mid - 1;
        }
        printf("%lld\n", l);
    }
    return 0;
}

L. WZB’s Harem

  • 这个题的题意就是n行n列的棋盘放置n个棋子,这些棋子不可以同行同列;并且开始会指定棋盘的某些位置不可以放置棋子。
  • 我们仍然是采用八皇后的棋子摆放策略(一行一行地摆,并且记录哪些列已经摆放了棋子),以及那个状态压缩 d p dp dp 经典问题( N ∗ M N*M NM 的矩阵摆满1*2的小木块的方案数)。
  • 思路其实就是状态压缩 d p dp dp f ( i , k ) f(i, k) f(i,k) 表示当前放置的是第 i i i 行,第二维的状态 k k k 表示的概念是当前已经有哪些摆放过了棋子。假设第 i i i 行,我们在第 j j j 列摆放了棋子。令 s t = 1 < < j st = 1 << j st=1<<j,那么状态转移方程就是 f ( i , k ∣ s t ) + = f ( i − 1 , k ) f(i, k | st) += f(i - 1, k) f(i,kst)+=f(i1,k).
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;

const int maxn = 25, maxm = (1 << 20) + 10;
int f[maxn][maxm], mz[maxn];
int N;
vector<int> cnt_one[maxn];
const int mod = 1000000007;

int main()
{
    scanf("%d", &N);
    for(int i = 1; i <= N; i++){
        for(int j = 0; j < N; j++){
            int x;
            scanf("%d", &x);
            if(x) mz[i] |= (1 << j);
        }
    }
    for(int i = 0; i < (1 << N); i++){
        cnt_one[__builtin_popcount(i)].push_back(i);
    }
    f[0][0] = 1;
    for(int i = 1; i <= N; i++){
        for(int j = 0; j < N; j++){
            int st = 1 << j;
            if((st & mz[i]) != 0) continue;
            for(auto k : cnt_one[i - 1]){
                if((k & st) != 0) continue;
                f[i][k | st] = (f[i][k | st] + f[i - 1][k]) % mod;
            }
        }
    }
    ll ans = f[N][(1 << N) - 1];
    for(ll i = 1; i <= N; i++){
        ans = ans * i % mod;
    }
    cout << ans << endl;
    return 0;
}

M. Zoos’s Animal Codes

  • 签到
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
string a, b;
int main()
{
    cin >> a >> b;
    cout << a + b;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值