2021“MINIEYE杯”中国大学生算法设计超级联赛(1)

2021“MINIEYE杯”中国大学生算法设计超级联赛(1)

导语

暑假第三次训练赛,选取能做的题来整理,不少题还是能做的,是练习太少的缘故

涉及的知识点

知识点

链接:2021“MINIEYE杯”中国大学生算法设计超级联赛(1)

题目

1001

题目大意:给出一个数n,计算 n % 1 ∣ n % 2 ∣ … ∣ n % ( n − 1 ) ∣ n % n n\%1 |n\% 2| \dots|n\%(n-1)|n\%n n%1n%2n%(n1)n%n,|为二进制或运算

思路:根据打表可得到规律,对于n,对应的结果都是某二次幂-1,所以关键就是获得这个二次幂,该二次幂其实为小于该数的最大二次幂,得到结论后就容易了,详见代码

代码

#include <bits/stdc++.h>

using namespace std;
int T;
long long n;
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>T;
    while(T--) {
        cin >>n;
        if(n==1) {//特判
            cout <<"0"<<endl;
            continue;
        }
        long long t=1;
        while(t<n)
            t<<=1;
        t>>=1;//获得二次幂
        t-=1;
        cout <<t<<endl;
    }
    return 0;
}

1005

题目大意:有编号2~n的一系列点,点之间的权重为编号的最小公倍数,找出该完全图的最小生成树的边权之和

思路:如果编号是素数显然与2的边构成的权值最小,如果是合数,将该点与先前连接的为该点因数的素数相连,构成的权值为自身,累和即可

代码

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
ll T;
bool IsPrime[10000001];//真值为素数
ll Prime[1000001],ans,sum[1000001];
void Choose(int n) { //筛选到n
    memset(IsPrime,1,sizeof(IsPrime));
    IsPrime[1]=IsPrime[0]=1;
    for(int i=2; i<=n; i++) {
        if(IsPrime[i]) {
            Prime[++ans]=i;
            sum[ans]=sum[ans-1]+i;
        }
        for(int j=1; j<=ans&&i*Prime[j]<=n; j++) {
            IsPrime[i*Prime[j]]=0;
            if(!i%Prime[j])
                break;
        }
    }
}
int main() {
    scanf("%lld",&T);
    Choose(1e7);
    while(T--) {
        ll n;
        scanf("%lld",&n);
        if(n==2)
            printf("0\n");
        else {
            ll len=upper_bound(Prime+1,Prime+ans+1,n)-Prime-1;
            ll res=sum[len]-2+(3+n)*(n-2)/2;
            printf("%lld\n",res);
        }
    }
    return 0;

1006

题目大意:给定一个n*m的矩阵,每个位置有整数值,对于任一一列,将从上到下连续不递增的数字连接在一起视为连通的线段,求由这些线段构成的最大子矩阵(矩阵只能有线段上的点)

思路:比赛的时候想着用DP,后来看了题解问了同学才知道,套用了一个以前就做过的一个模型:单调栈解直方图最大连续面积,参考文献中有链接,以每一行为基准,套用这个模型即可,详细见代码

代码

#include <bits/stdc++.h>

using namespace std;
int T,n,m,Matrix[2121][2121],area[2121],h[2121][2121];
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    //freopen("1.in","r",stdin);
    cin >>T;
    while(T--) {
        int ans=0;
        cin >>n>>m;
        for(int i=1; i<=n; i++)
            for(int j=1; j<=m; j++)
                cin >>Matrix[i][j];
        for(int i=1; i<=n; i++)
            for(int j=1; j<=m; j++)
                if(i==1||Matrix[i][j]<Matrix[i-1][j])
                    h[i][j]=1;
                else
                    h[i][j]=h[i-1][j]+1;
        //对于该点求得高度
        for(int k=1; k<=n; k++) {//单调栈处理该类题模板
            stack<int>S;
            int maxx=0,i=1;
            //每次插入时,计算出的是左边比待插入值大的个数,(比待插入值大的数)在自己位置右边比自己大的数
            while(i<=m) {
                if(S.empty()||h[k][i]>=S.top()) {//录入
                    S.push(h[k][i++]);
                    area[S.size()]=1;//area实际上是记录连续的可组成点的个数的
                } else {//遇到小于栈顶的值进行面积统计
                    int line=0;//用来统计行
                    while(!S.empty()&&S.top()>h[k][i]) {
                        line+=area[S.size()];//累积以当前值为基准,能够构成的左+右的连续矩阵宽度
                        maxx=max(maxx,S.top()*line);//计算以当前栈顶为基准能够得到的面积值
                        S.pop();
                    }
                    S.push(h[k][i++]);
                    area[S.size()]=line+1;//记录以当前值为基准,能够构成连续的左边矩阵的宽度
                }
            }
            int line=0;
            while(!S.empty()) {//防止一直上升,对栈非空情况进行处理
                line+=area[S.size()];
                maxx=max(maxx,S.top()*line);
                S.pop();
            }
            ans=max(maxx,ans);
            memset(area,0,sizeof(area));
        }
        cout <<ans<<endl;
        memset(Matrix,0,sizeof(Matrix));
        memset(h,0,sizeof(h));
    }
    return 0;
}

1008

题目大意:给出一个有n个数字的序列与一个整数k,判断是否存在一个连续子序列使得序列异或和大于等于k,如果有多个,输出左端点最小的序列的首下标和尾下标即可

思路:比赛的时候看到了异或,想起来最近看的字典树,但是畏畏缩缩没敢下手,毕竟不熟
首先本题需要构造前缀异或和,为什么呢,举个例子,对于L~R的异或和,直接用1 ~R的异或和与1 ~L-1的异或和异或即可,因为前面的区间都是相同的,异或之后为0,这样就能求出区间的连续异或值了
构造前缀异或和的字典树,但是每次插入的同时进行查询,求得当前插入的异或和值与字典树已各点(即不同的L)有值的异或和,如果走到一个节点发现接下来不论往左&右遍历,最终得到的异或和都大于等于k,直接返回覆盖该节点的最靠右的异或前缀和下标,即对于插入的异或和满足与其异或大于等于k并且最右的下标,否则返回-1,这样对于每个插入的值都能找到一个自身的最优左端点
需要特判只需要一个元素就可满足的情况,详见代码

关于覆盖该点的最右异或前缀和下标

经过与原作者讨论后,大致理解了为什么要返回该值,首先在已经构造的字典树中,当遍历到该结点时(即无论如何走都能大于等于k),此时已经得到的异或和为插入的前缀异或和的前i位与字典树中的前i位的异或和最大值,即在这一位上已经满足了题设条件,包含了这一结点的所有区间与插入前缀异或和的异或值都能满足题设条件,因为要选取区间长度最小,理所当然应该选取覆盖了该节点的的最右异或前缀和下标

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int t,n,k,trie[32*N][2],sum[N],data[N],cnt,mx[32*N],End[32*N];
void Insert(int x,int pos) {
    int p=0;
    for(int i=31; i>=0; i--) {
        int v=(x>>i)&1;//获取数的每一位
        mx[p]=max(mx[p],pos);//更新覆盖该节点的最右异或和下标
        if(!trie[p][v]) {
            trie[cnt][0]=trie[cnt][1]=0;//在构造的时候就进行清空
            End[cnt]=0;//同上
            trie[p][v]=cnt++;
        }
        p=trie[p][v];
    }
    End[p]=x;//记录这一个节点为一个数
    mx[p]=max(mx[p],pos);
}
int Query(int p,int s,int i,int x) {
    int ans=-1;
    if(s>=k)//如果异或和已经大于k,没有必要向下走
        return mx[p];
    if(s+((1ll<<(i+1))-1)<k)//如果当前异或和+剩下位数全取1仍然小于k,不存在值
        return -1;
    if(trie[p][0])//左子树有值
        ans=max(ans,Query(trie[p][0],s+(x&(1<<i)),i-1,x));//x&(1<<i)为提取i这一位对应的二进制数,与已经得到的异或和相加
    if(trie[p][1])//右子树有值
        ans=max(ans,Query(trie[p][1],s+((x&(1<<i))^(1<<i)),i-1,x));//x&(1<<i))^(1<<i)先提取,然后取反
    return ans;
}
signed main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>t;
    while(t--) {
        cin >>n>>k;
        cnt=1;//初始化
        trie[0][0]=trie[0][1]=0;//初始化
        int ansl=0,ansr=0,minlen=0x3f3f3f3f;//同上
        for(int i=0; i<=32*n; i++)//初始化
            mx[i]=0;
        for(int i=1; i<=n; i++) {
            cin >>data[i];
            sum[i]=sum[i-1]^data[i];//异或和
        }
        Insert(0,0);//插入根节点
        for(int i=1; i<=n; i++) {
            if(data[i]>=k) {//如果出现了一个数据直接大于k,选取这一个数据即可
                ansl=ansr=i;
                minlen=1;
                break;
            }
            int lpos=Query(0,0,31,sum[i]);//获得以当前前缀和为基准异或能不小于k的下标
            if(lpos!=-1&&((sum[i]^sum[lpos])>=k)) {
                int len=i-lpos;
                if(len<minlen) {//更新最值
                    minlen=len;
                    ansl=lpos+1,ansr=i;
                } else if(len==minlen)
                    if(ansl>lpos+1)
                        ansl=lpos+1,ansr=i;
            }
            Insert(sum[i],i);//插入该点的前缀异或和
        }
        if(minlen!=0x3f3f3f3f)//存在最值
            cout <<ansl<<" "<<ansr<<endl;
        else
            cout <<-1<<endl;
    }
    return 0;
}

1009

题目大意:给一张图,设有一个D,使得图中边小于等于D并相连的节点可以通过该边合并,大于D的边两节点不能通过该边合并,最后使整个图被分为k个集合,求最小D

思路:因为求最小D,那么肯定是以某一边值划分即可,从小到大选边,若选出边的两点为一个集合则跳过,如果不为一个集合且合并后集合数正好为k,D暂定当前边权,并合并,如果D已有值并且下一条边就是D值,说明需要继续合并,但当前集合数已经到k,合并后必然无法满足条件,无解,如果D已有值且下一值比D大,说明已经划分完成

#include <bits/stdc++.h>
#define int long long
using namespace std;
int K,T,n,m,k,fa[121212];
typedef struct node {//存边
    int u,v,c;
    node(int a,int b,int t) {
        u=a;
        v=b;
        c=t;
    }
    bool operator<(node a)const {
        return c>a.c;
    }
} node;
int Find(int x) {//压缩路径
    if(x==fa[x])
        return x;
    return fa[x]=Find(fa[x]);
}
signed main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>T;
    while(T--) {
        cin >>n>>m>>k;
        priority_queue<node,vector<node>,less<node>>Q;
        while(m--) {
            int u,v,c;
            cin >>u>>v>>c;
            Q.push(node(u,v,c));
        }
        for(int i=1; i<=n; i++)
            fa[i]=i;
        int ans=n,D=0x3f3f3f3f3f3f3f3f;
        if(k==n)//刚好分完
            D=0;
        bool flag=1;
        while(!Q.empty()) {
            if(D==0)
                break;
            node t=Q.top();
            Q.pop();
            if(Find(t.u)==Find(t.v))//如果属于同一个集合跳过
                continue;
            if(ans==k+1)//如果合并完后刚好为k个,记录备选答案
                D=t.c;
            else if(ans==k&&t.c==D) {//如果此时已经有k个集合,但是下一条边也是为D,代表还要合并,会使得ans变小
                flag=0;
                break;
            } else if(ans==k&&t.c>D)//如果此时有k个集合,下一条边比D大,后面的都比D大,代表已经划分完成
                break;
            fa[Find(t.u)]=Find(t.v);//合并
            ans--;
        }
        if(!flag||D==0x3f3f3f3f3f3f3f3f)//判断是否划分成功
            cout <<-1<<endl;
        else
            cout <<D<<endl;
    }
    return 0;
}

参考文献

  1. 2021“MINIEYE杯”中国大学生算法设计超级联赛(1)1006 / HDU6955. Xor sum(01Trie/好题)
  2. 2021杭电多校1
  3. ACM Weekly 3(待修改)
  4. 2021“MINIEYE杯”中国大学生算法设计超级联赛(1)1009. KD-Graph(并查集)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值