2023牛客暑假多校第五场(补题向题解:C,E,B, I)

当时只做出来4个题,被两个学弟的队伍压了一题,可能是因为两个中等题都偏向构造和猜结论这种,我们队伍不太擅长,需要加强这方面的训练。

C Cheeeeen the Cute Cat(图论)

说实话不看题解,还是很难想到这么转化的,当时队友直接用正解过了这个题,tql
知识点:二分图匹配,哈密顿图,半哈密顿图,竞赛图

题意

给定一组二分图匹配在 ( 1 ∼ n ) (1\sim n) (1n) ( n + 1 ∼ n + n ) (n+1\sim n + n) (n+1n+n) 之间,求最大匹配数,给定匹配满足不存在 i − ( i + n ) i-(i+n) i(i+n),以及若存在 i − ( j + n ) i-(j+n) i(j+n) 就一定不存在 j − ( i + n ) j -(i+n) j(i+n)(关键)。 n ≤ 3000 n\leq3000 n3000,匹配形式以邻接矩阵给出,并保证有 n ∗ ( n − 1 ) 2 \frac{n*(n-1)}{2} 2n(n1) 个匹配。

思路

直接用二分图匹配或者网络流来跑肯定不现实,时间复杂度不允许,考虑如何转化。

建图,将一个匹配 i → j + n i \rightarrow j+ n ij+n 视作 i → j i \rightarrow j ij 有一条有向边,二分图匹配成功一对可以看做经过一条边,唯一匹配则要求点不能重复经过。
而题目给出图转换后满足无自环无重边,并且边数为 n ∗ ( n − 1 ) 2 \frac{n*(n-1)}{2} 2n(n1)。这说明转化后的图是竞赛图,而竞赛图的性质是一定存在一条哈密顿通路,即一定能不重复的经过 n n n 个点走过 n − 1 n-1 n1 条边,根据上述转化最少的匹配数也是 n − 1 n-1 n1.

而考虑到走出一个回路也是合法的匹配,则答案是否为 n n n 则需要判定是否为哈密顿图(具有哈密顿回路),而竞赛图是否具有哈密顿回路则需要判断是否强连通,即判断是否所有强连通分量大小都 > 1 > 1 >1 这样就可以形成若干哈密顿回路使得答案为 n n n.

具体如何实现,tarjan算法缩点计数即可。

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 3010;
int n, dfn[N], low[N], vis[N], st[N], cnt, top;
vector<int> g[N];

int min_cnt = 1e9; // 最小的强连通分量的大小
void tarjan(int u){
    vis[u] = 1; st[top ++] = u;
    dfn[u] = low[u] = ++ cnt;

    for(auto v : g[u]){
        if(!dfn[v]){
            tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if(vis[v]){
            low[u] = min(low[u], dfn[v]);
        }
    }

    if(dfn[u] == low[u]){
        int sum = 0;
        while(true){
            int x = st[-- top]; vis[x] = 0;
            sum ++;
            if(x == u) break;
        }
        min_cnt = min(min_cnt, sum);
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n;
    for(int i = 1; i <= n; i ++){
        for(int j = 1; j <= n; j ++){
            int x; cin >> x;
            if(x) g[i].push_back(j); // 建图
        }
    }

    for(int i = 1; i <= n; i ++){
        if(!dfn[i]) tarjan(i);
    }
    if(min_cnt < 2) cout << n - 1 << "\n";
    else cout << n << "\n";
    return 0;
}

E Red and Blue and Green(构造,贪心)

当时摆烂,遇到构造就不想动脑子了,今天回过头重新写发现还是能不看题解写出来的,只是效率偏低。

题意

需要构造一个大小为 n n n 的排列,使得满足 m m m 个要求, m m m 个要求以 [ l , r , w ] [l,r,w] [l,r,w] 的形式给出,要求区间 [ l , r ] [l,r] [l,r] 的逆序对数的奇偶为 w w w. 给定的 m m m 个区间保证不重复,且任意两个区间都不相交(可能包含)。

思路

首先我们需要知道,对于一个排列的任意一个区间 [ l , r ] [l, r] [l,r],任意交换两个位置 x , y x, y x,y 上的数,只要满足 x , y x, y x,y 不全包含在 [ l , r ] [l, r] [l,r] 中,则不会改变区间 [ l , r ] [l, r] [l,r] 的逆序对。
题目既然只有不相交和包含关系,于是就可以建出一个树(有包含关系的区间为父子)递归解决子问题。例如 1. [ 1 , 6 ] , 2. [ 2 , 4 ] , 3. [ 2 , 3 ] 1.[1,6],2.[2,4],3.[2,3] 1.[1,6],2.[2,4],3.[2,3],其中2为1的儿子,3为2的儿子,2是1的一级子区间,3是1的二级子区间。

递归到 u u u 区间时,统计所有一级子区间 v i v_i vi 的逆序对奇偶,假设已经满足了所有的子区间 v i v_i vi 的要求,加起来以后若已经满足区间 u u u 的要求则不作改变,否则只需要找到该区间 u u u 中相邻两个数(不在同一个一级子区间 v i v_i vi 中)交换,这样只会对 u u u 这个区间逆序对改变 1 1 1,而不会影响到任何一个子区间的逆序对数。

而我们可以在开头就预处理这些信息,在解决子问题前就先交换。

注意无解的情况当且仅当 [ l , r , w ] , l = r , w = 1 [l, r, w],l=r,w=1 [l,r,w]l=rw=1.

具体实现看代码

代码

/* 
对于一个排列的任意一个区间[l, r],任意交换两个数x, y, 只要满足x, y不全在[l, r] 中, 则不会改变区间[l, r]的逆序对
只有不相交和包含关系,于是建出一个树递归解决子问题
递归到u区间时,统计所有一级子区间的逆序对奇偶,若满足则不作改变,
否则只需要找到该区间相邻两个数(不在同一个一级子区间中)交换,这样只会对u这个区间逆序对改变1,而不会影响到任何一个子区间

而我们可以在开头就预处理这些信息,在解决子问题前就先交换
*/
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

struct seg{
    int l, r, w;
    bool operator < (const seg& A)const{
        return r - l + 1 > A.r - A.l + 1; // 按区间大小排序
    }
}s[N];

bool cmp(int A, int B){
    return s[A].l < s[B].l;
}

vector<int> g[N];
int p[N], sum[N], len[N], siz[N];
void dfs(int u, int w){
    if(sum[u] != w){ // 子区间满足的情况下,当前区间不满足,则需要只对当前区间改变不影响子区间,可以提前交换
        if(siz[u]){
            if(len[u] == s[u].r - s[u].l + 1) swap(p[s[g[u][0]].r], p[s[g[u][1]].l]); // 子区间已满则直接交换两个相邻子区间的相邻的数
            else if(s[g[u][0]].l == s[u].l) swap(p[s[g[u][0]].r], p[s[g[u][0]].r + 1]);
            else swap(p[s[g[u][0]].l], p[s[g[u][0]].l - 1]);
        }
        else swap(p[s[u].l], p[s[u].l + 1]);
    }
    for(auto v : g[u]){
        dfs(v, s[v].w);
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    int n, m;
    cin >> n >> m;
    
    for(int i = 1; i <= m; i ++){
        int l, r, w;
        cin >> l >> r >> w;
        if(l == r && w == 1){
        	cout << "-1\n";
        	return 0;
        }
        s[i] = {l, r, w};
    }

    sort(s + 1, s + 1 + m);
    for(int i = m; i >= 1; i --){
    	bool flag = 0;
        for(int j = i - 1; j >= 1; j --){
            if(s[j].l <= s[i].l && s[j].r >= s[i].r){ // 建树
                g[j].push_back(i); flag = 1;
                siz[j] ++;
                sum[j] ^= s[i].w; // 预处理一级子区间已经满足的情况下的当前区间的奇偶
                len[j] += s[i].r - s[i].l + 1;
                break;
            }
        }
        if(!flag) {
            g[0].push_back(i); // 虚构一个总区间 [1, n]
            sum[0] ^= s[i].w;
        }
    }

    for(int i = 1; i <= n; i ++) p[i] = i; // 先设定好初始逆序对为0的排列
    for(int i = 1; i <= m; i ++) sort(g[i].begin(), g[i].end(), cmp); // 同一级的子区间按左端点排序
    
    int st = (s[1].l == 1 && s[1].r == n) ? 1 : 0;
    s[0].w = sum[0];
    dfs(st, s[st].w);
    
    for(int i = 1; i <= n; i ++) cout << p[i] << " ";
    return 0;
}

B Circle of Mistery(贪心,构造)

题意

n n n 个点,点 i i i 的权值为 a i a_i ai,要求构造一个长度为 n n n 的排列 p p p,生成 p i p_i pi i i i 连边的无向图,使得至少存在一个简单环使得环上点权和 ≥ k \geq k k. 同时要求构造的排列逆序对最小,输出最小的逆序对数,无解输出 − 1 -1 1. 1 ≤ n ≤ 1 0 3 , − 1 e 6 ≤ a i , k ≤ 1 e 6 1\leq n\leq 10^3,-1e6\leq a_i,k\leq 1e6 1n103,1e6ai,k1e6.

思路

考虑我们一定是选择一个区间,舍弃其中(可能为 0 0 0)一些数,使得成环后 s u m ≥ k sum \geq k sumk.

  1. 假设都是正数,不用舍弃那么最优构造将成环区间所有数右移一位即可,代价为区间长度 − 1 -1 1.
  2. 考虑要舍弃一些负数,除了舍弃的数其他数按1.方法构造仍然最优,对于舍弃的数每个都会比原先多贡献 1 1 1 的逆序对数。

于是我们的策略就是枚举区间的右端点,找到第一个满足区间正数之和 ≥ k \geq k k 的左端点,将所有的负数当做舍弃的数。继续向左推进左端点,每增加一个数,从被舍弃的数中判断最多能取出多少的负数,使得代价减少,这些负数用动态开点权值线段树维护,并将负数离散化为对应正数 − x → x -x \rightarrow x xx,贪心的考虑每次取回最大的负数才能取回的更多,线段树中二分求解。

具体见代码

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 1e3 + 10, MAX = 1e6;
int a[N];

struct node{
    int ls, rs, sum, val;
}tr[N * 40];

int tot, root;

void clear(){ // 清空
    for(int i = 1; i <= tot; i ++) tr[i] = {0, 0, 0, 0};
    tot = root = 0;
}
void update(int &p, int loc, int k, int l = 0, int r = MAX){ // 更新
    if(!p) p = ++ tot;
    tr[p].sum ++; // 更新负数的数量
    tr[p].val += k; // 更新负数的和
    if(l == r) return ;
    int mid = (l + r) >> 1;
    if(loc <= mid) update(tr[p].ls, loc, k, l, mid);
    else update(tr[p].rs, loc, k, mid + 1, r);
}

int get_sum(int p, int k, int l = 0, int r = MAX){ // 最多能取回和为k的负数
    if(!p || !k) return 0;
    if(l == r){
        if(tr[p].val >= k) return tr[p].sum;
        return 0;
    }
    int mid = (l + r) >> 1;
    if(tr[tr[p].ls].val < k) return get_sum(tr[p].ls, k, l, mid);
    else return tr[tr[p].ls].sum + get_sum(tr[p].rs, k - tr[tr[p].ls].val, mid + 1, r);
}

int main(){
    int n, k;
    cin >> n >> k;
    for(int i = 1; i <= n; i ++) cin >> a[i];

    if(*max_element(a + 1, a + 1 + n) >= k){ // 自环即可
        cout << "0\n";
        return 0;
    }

    if(k <= 0){ // 全是负数
        cout << "-1\n";
        return 0;
    }

    int ans = MAX;
    for(int r = 1; r <= n; r ++){

        clear();
        
        int val = 0, res = MAX, sum = 0;
        for(int l = r; l >= 1; l --){
            if(a[l] >= 0) val += a[l], sum ++;
            else update(root, -a[l], a[l]);
            if(val >= k) res = min(res, r - l + (r - l + 1 - sum) - get_sum(root, -(val - k)));
        }
        ans = min(ans, res);
    }
    if(ans == MAX) cout << "-1\n";
    else cout << ans << "\n";
    return 0;
}

I The Yakumo Family(DP,拆位,前缀和)

很妙的一个妙妙dp,看着别人看jiangly代码写出来的题解,再折磨补过这题的队友给我讲题,终于弄明白了一点,试图用自己语言解释一下。

题意

给定长度为 n n n 的数组 a a a,设 X O R [ l , r ] = a l ⨁ a l + 1 ⨁ ⋯ ⨁ a r − 1 ⨁ a r XOR[l,r] = a_l \bigoplus a_{l+1}\bigoplus \cdots \bigoplus a_{r-1}\bigoplus a_r XOR[l,r]=alal+1ar1ar. 求 ∑ 1 ≤ l 1 ≤ r 1 < l 2 ≤ r 2 < l 3 ≤ r 3 ≤ n X O R [ l 1 , r 1 ] ∗ X O R [ l 2 , r 2 ] ∗ X O R [ l 3 , r 3 ] \sum_{1\leq l_1 \leq r_1<l_2\leq r_2<l_3\leq r_3\leq n}XOR[l_1,r_1]*XOR[l_2,r_2]*XOR[l_3,r_3] 1l1r1<l2r2<l3r3nXOR[l1,r1]XOR[l2,r2]XOR[l3,r3].

思路

如题,是要找到所有可能的 3 3 3 个不重叠的子数组的异或和之积的和。我们先考虑一个简化的问题,求一个子数组的所有可能异或和之和。

首先我们知道位运算的一个性质,对于二进制每一位是单独运算互不影响的,所以我们拆位来考虑。

我们首先枚举区间的右端点 r r r, 以二进制第 k k k 位为例,如何算 2 k 2^k 2k 的贡献,一个暴力的方法就是枚举区间的左端点 l l l 若是 X O R [ l , r ] XOR[l,r] XOR[l,r] 二进制第 k k k 位为 1 1 1 就能算一次贡献 1 ∗ 2 k 1*2^k 12k,如何优化这个过程?

考虑前缀和优化,设 p r e [ i ] = X O R [ 1 , i ] pre[i] = XOR[1, i] pre[i]=XOR[1,i] 即前缀异或和, s u m [ k ] [ 0 / 1 ] : sum[k][0/1]: sum[k][0/1]: 动态更新代表前 i i i 个数中有多少个前缀异或和 p r e [ l ] pre[l] pre[l] 的二进制第 k k k 位为 0 / 1 0/1 0/1. 那么当我们枚举右端点 r r r 时,枚举到二进制第 k k k 位的贡献有,若 p r e [ i ] pre[i] pre[i] k k k 位为 p p p,那么异或上 p r e [ l ] pre[l] pre[l] k k k ! p !p !p 的前缀就能贡献 2 k 2^k 2k,即 a n s + s u m [ k ] [ ! p ] ∗ 2 k ans + sum[k][!p] * 2^k ans+sum[k][!p]2k.

代码如下

for(int i = 1; i <= n; i ++){
    cin >> a[i];
    pre[i] = pre[i - 1] ^ a[i]; // 前缀异或和
}

int ans = 0;
for(int r = 1; r <= n; r ++){ // 枚举区间右端点
    for(int j = 0; j < 31; j ++){ // 枚举二进制各位的贡献
        ans += sum[j][!(pre[r] >> j & 1)] * (1 << j);
    }    
    for(int j = 0; j < 31; j ++){ // 动态更新
        sum[j][pre[r] >> j & 1] += 1;
    }
}

回到我们本次的问题,我们考虑分成 2 2 2 个子数组如何解决,不知道是否有注意到在前文说贡献和代码中计算 s u m sum sum 的时候用的是 1 ∗ 2 k 1*2^k 12k 以及 s u m + = 1 sum += 1 sum+=1,我们此时将 s u m sum sum 的定义改成:动态更新前 i i i 个数分出 1 1 1 个子数组的异或和,那么此时 s u m sum sum 的贡献就不是数量了,而是前一段子数组的异或和,设 X O R [ l 2 , r 2 ] = 2 p 1 + 2 p 2 + ⋯ + 2 p k XOR[l_2,r_2] = 2^{p_1}+2^{p_2}+\dots+2^{p^k} XOR[l2,r2]=2p1+2p2++2pk,那么 X O R [ l 1 , r 1 ] ∗ X O R [ l 2 , r 2 ] XOR[l_1,r_1]*XOR[l_2, r_2] XOR[l1,r1]XOR[l2,r2] 一样可以写成 X O R [ l 1 , r 1 ] ∗ ( 2 p 1 + 2 p 2 + ⋯ + 2 p k ) XOR[l_1,r_1]*(2^{p_1}+2^{p_2}+\dots+2^{p^k}) XOR[l1,r1](2p1+2p2++2pk) 拆位计算同样不影响结果。而 X O R [ l 1 , r 1 ] XOR[l_1,r_1] XOR[l1,r1] 就是我们现在定义的 s u m sum sum.

同理我们只需要再更改一次 s u m sum sum 的定义,将其变成前 i i i 个数 分成 j j j 段的异或和之积的和。就能处理 3 3 3 段子数组甚至更多子数组的问题。

代码

#include <bits/stdc++.h>
using namespace std;

#define ll long long
const int N = 2e5 + 10, mod = 998244353;

int n, w[N], p[N];
ll f[4][N], sum[4][31][2]; 
// f:前i个数,分成不重合的j段 j段异或和之积的和
// sumjkp:动态更新,前 i 个数 分成 j - 1段 前缀二进制第k项为p的 异或和之积的和

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n;
    for(int i = 1; i <= n; i ++){
        cin >> w[i];
        p[i] = p[i - 1] ^ w[i]; // 前缀异或和
    }

    for(int i = 0; i <= n; i ++) f[0][i] = 1;

    for(int i = 1; i <= 3; i ++){ // 枚举这是第几次分段
        for(int r = 0; r <= n; r ++){ // 枚举第 i 段的右端点为 r
            for(int k = 0; k < 31; k ++){ // 拆位计算
                f[i][r] = (f[i][r] + sum[i][k][!(p[r] >> k & 1)] * (1LL << k) % mod) % mod;
            }
            for(int k = 0; k < 31; k ++){
                sum[i][k][p[r] >> k & 1] = (sum[i][k][p[r] >> k & 1] + f[i - 1][r]) % mod; // 将前i-1段异或和之积的结果累加进来
            }
        }
        for(int j = 1; j <= n; j ++){
            f[i][j] = (f[i][j] + f[i][j - 1]) % mod; // 所有子数组,就要求右端点任意,所以累加
        }
    }
    cout << f[3][n] << "\n";
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值