2025“钉耙编程”中国大学生算法设计春季联赛(3)部分题解

  • 1001 数列计数

一个结论:\(C_{n}^{m}\)为奇数当且仅当\(n & k==k\)
于是就可以分类讨论一下,当\(l_{i}\)大于等于\(a_{i}\),此时\([0,a_{i}]\)全都可以取到,假定二进制下\(a_{i}\)\(k\)个1,方案数就是\(2^{k}\)

否则,我们就需要统计出\([0,l_{i}]\)中的,二进制下出现了1的位置都是\(a_{i}\)二进制下1的位置的数,我用的是数位dp。设\(dp[i][0/1][0/1]\)表示从高到低第\(i\)位取\(0/1\)得情况下,当前数是否等于\(l_{i}\),由于\(a_{i}\)在某一位是0时,那么这一位就不能取1,于是我们可以推出状态转移方程(详见代码

点击查看代码
#include<bits/stdc++.h>
    
using namespace std;

const long long mod = 998244353;
long long t;
const long long N = 2e5 + 10;
long long n,a[N],b[N];

long long quickMul(long long x,long long k) {
    if(k == 0) return 1;
    long long tmp = quickMul(x,k / 2);
    tmp = (tmp * tmp) % mod;
    if(k & 1) tmp = (tmp * x) % mod;
    return tmp;
}
    
void solve() {
    cin >> n;
    for(long long i = 1;i <= n;i++) cin >> a[i];
    for(long long i = 1;i <= n;i++) cin >> b[i];
    long long ans = 1;
    for(long long i = 1;i <= n;i++) {
        long long tmp = 0;
        if(a[i] <= b[i]) {
            tmp = a[i];
            ans = (ans * (quickMul(2,__builtin_popcount(tmp)))) % mod;
        }
        else {
            vector<long long> digit1,digit2;
            long long dp[110][2][2];
            long long temp = a[i],temb = b[i];
            while(temp) {
                digit1.push_back(temp % 2);
                digit2.push_back(temb % 2);
                temp /= 2;
                temb /= 2;
            }
            long long len = digit1.size() - 1;
            memset(dp,0,sizeof dp);
            if(digit2[len] == 1) {
                dp[len][1][1] = 1;
                dp[len][0][0] = 1;
            }
            else dp[len][0][1] = 1;
            for(long long i = len - 1;i >= 0;i--) {
                if(digit1[i] == 1) {
                    if(digit2[i] == 1) {
                        dp[i][1][1] = (dp[i + 1][1][1] + dp[i + 1][0][1]) % mod;
                        dp[i][1][0] = (dp[i + 1][0][0] + dp[i + 1][1][0]) % mod;
                        dp[i][0][0] = (dp[i + 1][1][1] + dp[i + 1][0][0] + dp[i + 1][0][1] + dp[i + 1][1][0]) % mod;
                    }
                    else {
                        dp[i][0][1] = (dp[i + 1][1][1] + dp[i + 1][0][1]) % mod;
                        dp[i][1][0] = (dp[i + 1][1][0] + dp[i + 1][0][0]) % mod;
                        dp[i][0][0] = (dp[i + 1][1][0] + dp[i + 1][0][0]) % mod;
                    }
                }
                else {
                    if(digit2[i] == 1)
                        dp[i][0][0] = (dp[i + 1][1][1] + dp[i + 1][0][0] + dp[i + 1][0][1] + dp[i + 1][1][0]) % mod;
                    else {
                        dp[i][0][1] = (dp[i + 1][1][1] + dp[i + 1][0][1]) % mod;
                        dp[i][0][0] = (dp[i + 1][1][0] + dp[i + 1][0][0]) % mod;
                    }
                }
            }
            tmp = (dp[0][0][0] + dp[0][0][1] + dp[0][1][1] + dp[0][1][0]) % mod;
            ans = (ans * tmp) % mod;
        }
        
    }
    cout << ans << '\n';
}
    
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> t;
    while(t--) solve();
    
    return 0;
}
  • 1003 拼尽全力

(拼尽全力无法战胜
我们可以直接对于每一项能力建一个优先队列,那么只需要不断的重复一下过程:当前这一项能力能通过的就通过,并且在对应\(i\)公司的统计数组\(vis[i]\)加上1,表示这个公司可以在某一顺序下能力被达到\(vis[i]\)项,如果\(vis[i]\)为m,表示这个公司面试可以通过,那么就给每个能力值都加上提升的值。一直重复到没有面试可以通过,最后只需检查是不是所有的公司能达到的项数都是m即可。

点击查看代码
#include<bits/stdc++.h>
    
using namespace std;
    
long long t;
const long long N = 1e6+ 10;
long long n,m,vis[N];
struct node {
    long long val,pos;
    bool operator < (const node &a) const {
        return val > a.val;
    }
};
priority_queue<node> q[N];
long long a[N];
    
void solve() {
    for(long long i = 0;i <= max(n,m);i++) {
        while(!q[i].empty()) q[i].pop();
        vis[i] = 0;
    }
    cin >> n >> m;
    vector<vector<long long>> c(n + 1,vector<long long>(m + 1));
    vector<vector<long long>> w(n + 1,vector<long long>(m + 1));
    for(long long i = 1;i <= m;i++) cin >> a[i];
    for(long long i = 1;i <= n;i++) {
        for(long long j = 1;j <= m;j++) cin >> c[i][j],q[j].push({c[i][j],i});
        for(long long j = 1;j <= m;j++) cin >> w[i][j];
    }
    bool flag;
    while(1) {
        flag = false;
        for(long long j = 1;j <= m;j++) {
            while(!q[j].empty() && q[j].top().val <= a[j]) {
                flag = true;
                vis[q[j].top().pos]++;
                if(vis[q[j].top().pos] == m) {
                    for(long long i = 1;i <= m;i++)
                        a[i] += w[q[j].top().pos][i];
                }
                q[j].pop();
            }
        }
        if(!flag) break;
    }
    flag = false;
    for(long long i = 1;i <= n;i++) if(vis[i] != m) flag = true;
    cout << (flag == false ? "YES\n" : "NO\n");
}
    
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> t;
    while(t--) solve();
    
    return 0;
}
  • 1004 弯曲筷子

贪心着想,一根筷子需要跟长度和它最接近的筷子一起才能让不舒适度最小,所以我们可以先让所给序列排序。接着考虑dp,设dp[i][0/1][0/1]表示当前位于第i根筷子,当前筷子取/不取的情况下,目前所取筷子总数是否是奇数的最小不舒适度。若当前不取,那么前一根筷子若取了就变成奇数的情况下就不能算,因为若上一根筷子取了并且让取的筷子数量变为奇数,那么由于目前这根筷子长度与它最接近,又当前筷子不取,所以得到的结果一定不是最优。这样我们就可以写出状态转移方程(详见代码)。然而有一种特殊情况,即对于两根一定要取得筷子,有可能直接取他们两个最优而不是通过取别的来分别与他们配对(例如1 3 7 100,要求1 7必取,那么肯定是直接取这两根筷子最优),所以我们只需要记录下上一根必须取得筷子所在位置,当遇到下一个必须取得筷子时,和这种特殊情况取最优即可

点击查看代码
#include<bits/stdc++.h>
    
using namespace std;
    
long long t;
const long long N = 3e5 + 10;
long long n,m,p[N];
struct node {
    long long val,pos;
    bool operator < (const node &a) const {
        return val < a.val;
    }
}in[N];
    
void solve() {
    for(long long i = 0;i <= n;i++) p[i] = in[i].val = in[i].pos = 0;
    cin >> n >> m;
    for(long long i = 1;i <= n;i++) cin >> in[i].val,in[i].pos = i;
    for(long long v,i = 1;i <= m;i++) cin >> v,p[v] = 1;
    sort(in + 1,in + 1 + n);
    long long dp[n + 1][2][2];
    for(long long i = 0;i <= n;i++)
        dp[i][0][0] = dp[i][0][1] = dp[i][1][0] = dp[i][1][1] = 9e18;
    dp[0][0][0] = 0;
    long long pre = -1;
    for(long long i = 1;i <= n;i++) {
        dp[i][1][1] = min(dp[i - 1][0][0],dp[i - 1][1][0]);
        if(i > 1) dp[i][1][0] = dp[i - 1][1][1] + (in[i].val - in[i - 1].val) * (in[i].val - in[i - 1].val);
        if(!p[in[i].pos]) dp[i][0][0] = min(dp[i - 1][0][0],dp[i - 1][1][0]);
        if(p[in[i].pos]) {
            if(~pre) dp[i][1][0] = min(dp[i][1][0],dp[pre][1][1] + (in[i].val - in[pre].val) * (in[i].val - in[pre].val));
            pre = i;
        }
    }
    cout << min(dp[n][1][0],dp[n][0][0]) << '\n';
}
    
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> t;
    while(t--) solve();
    
    return 0;
}
  • 1005 修复公路

签到题,判连通块或者用并查集都可以,就不放代码了

  • 1007 宝石商店

可持久化trie树。与主席树类似的,我们可以在每新加一个树的时候,在仅仅需要修改的那一条路径上新加点从而达到可持久化的目的。价值的计算方式其实就是异或。然后求两个数最大异或只需要在trie树上贪心的走与当前数当前位不同的方向即可

点击查看代码
#include<bits/stdc++.h>
    
using namespace std;
    
int t;
const int N = 2e6 + 10;
int n,q,root[N << 5],tot;
int a[N];
int ch[N << 5][2],val[N << 5];

void insert(int p,int k) {
    int rt = ++tot;
    for(int i = 31;i >= 0;i--) {
        val[rt] = val[p] + 1;
        int c = (k & (1ll << i) ? 1 : 0);
        if(!ch[rt][c]) ch[rt][c] = ++tot;
        ch[rt][!c] = ch[p][!c];
        rt = ch[rt][c];
        p = ch[p][c];
    }
    val[rt] = val[p] + 1;
}

int query(int u,int p,int x) {
    int res = 0;
    for(int i = 31;i >= 0;i--) {
        int c = (x & (1ll << i) ? 1 : 0);
        if(val[ch[u][!c]] - val[ch[p][!c]] > 0) {
            u = ch[u][!c];
            p = ch[p][!c];
            res += (1ll << i);
        }
        else {
            u = ch[u][c];
            p = ch[p][c];
        }
    }
    return res;
}
    
void solve() {
    for(int i = 0;i <= tot;i++) val[i] = ch[i][0] = ch[i][1] = 0;
    tot = 0;
    cin >> n >> q;
    for(int i = 1;i <= n;i++) {
        cin >> a[i];
        root[i] = tot + 1;
        insert(root[i - 1],a[i]);
    }
    for(int l,r,x,i = 1;i <= q;i++) {
        cin >> l >> r >> x;
        cout << query(root[r],root[l - 1],x) << '\n';
    }
}
    
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> t;
    while(t--) solve();
    
    return 0;
}
  • 1009 部落冲突

并查集操作。与传统的并查集不同的是,该题多了让某个人转移和两部落交换的操作。鉴于后者,我们当然不能直接遍历每个人然后把他移到另一个部落,所以考虑将部落编号交换,于是可以用双向映射,即建两个数组pos[i]和mp[i],并引入一个中间值,假设最开始每个部落i都在i位置上,那么pos[i]表示此时i位置对应的部落是pos[i],而mp[i]则表示部落i此时所在的位置是mp[i]。而对于fat[i]所指向的就应该是“位置”而非“部落”。这里用扩展域并查集,1-n范围表示野蛮人所在的位置,n+1-2n所表示的是位置的fat
对于操作1,我们只需让位置b的fat指向位置a
对于操作2,我们只需让野蛮人a的fat指向位置b
对于操作3,我们只需让\(pos[mp[a+n]]\)\(pos[mp[b+n]]\)交换,再把\(mp[a+n]\)\(mp[b+n]\)交换即可
对于操作4,直接输出查询答案

点击查看代码
#include<bits/stdc++.h>
    
using namespace std;
    
int t;
const int N = 4e6 + 10;
int fat[N],pos[N],mp[N];
int n,q;

int find(int x) {
    if(fat[x] != x) fat[x] = find(fat[x]);
    return fat[x];
}
    
void solve() {
    for(int i = 0;i <= 2 * n;i++) fat[i] = mp[i] = pos[i] = 0;
    cin >> n >> q;
    for(int i = 1;i <= 2 * n;i++) {
        if(i <= n) fat[i] = i + n;
        else fat[i] = i;
    }
    for(int i = 1 + n;i <= 2 * n;i++) pos[i] = i,mp[i] = i;
    for(int pd,a,b,i = 1;i <= q;i++) {
        cin >> pd;
        if(pd == 1) {
            cin >> a >> b;
            fat[mp[b + n]] = fat[mp[a + n]];
        }
        else if(pd == 2) {
            cin >> a >> b;
            fat[a] = mp[b + n];
        }
        else if(pd == 3) {
            cin >> a >> b;
            swap(pos[mp[a + n]],pos[mp[b + n]]);
            swap(mp[a + n],mp[b + n]);
        }
        else {
            cin >> a;
            cout << pos[find(a)] - n << '\n';
        }
    }
}
    
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> t;
    while(t--) solve();
    
    return 0;
}
  • 1010 选择配送

求最大曼哈顿距离最小值,从曼哈顿距离的式子推导:

$ \left | x - a_{i}\right |+\left | y-b_{i}\right |$

可以分成两种情况:

$ \left | (x+y) - (a_{i}+b_{i})\right |$

或者
$ \left | (x-y) - (a_{i}-b_{i})\right |$

于是对于所有的客户,我们只需要维护四个值min1,max1,min2,max2分别表示\((x_{i}+y_{i})\)的最小/最大和\((x_{i}-y_{i})\)的最小/最大,然后再遍历所有的配送站,求出最大距离的最小值即可

点击查看代码
#include<bits/stdc++.h>
    
using namespace std;
    
long long t;
const long long N = 1e6 + 10;
long long n,m;
struct node {
    long long x,y;
}in[N],st[N];
    
void solve() {
    cin >> n >> m;
    long long min1 = 1e18,min2 = 1e18,max1 = -1e18,max2 = -1e18;
    for(long long i = 1;i <= n;i++) {
        cin >> in[i].x >> in[i].y;
        min1 = min(min1,in[i].x + in[i].y);
        min2 = min(min2,in[i].x - in[i].y);
        max1 = max(max1,in[i].x + in[i].y);
        max2 = max(max2,in[i].x - in[i].y);
    }
    long long ans = 1e18;
    for(long long i = 1;i <= m;i++) {
        cin >> st[i].x >> st[i].y;
        ans = min(ans,max({abs(st[i].x + st[i].y - min1),abs(st[i].x - st[i].y - min2),abs(st[i].x + st[i].y - max1),abs(st[i].x - st[i].y - max2)}));
    }
    cout << ans << '\n';
}
    
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> t;
    while(t--) solve();
    
    return 0;
}
posted @ 2025-03-22 16:43  孤枕  阅读(547)  评论(0)    收藏  举报