2021“MINIEYE杯”中国大学生算法设计超级联赛 第一场题解

ACM暑期多校联合赛 题解

6950 Mod, Or and Everything

 签到题

  题意:给定一个整数N,求(n mod 1) or (n mod 2) or ... or (n mod (n - 1)) or (n mod n).

  思路:
  “肯定是先打表找规律啊!”

  于是,我们会发现:
  当N和答案的关系是,求K = log(n - 1)/log(2) + 1(一个K,使得N 恰好 小于 (1<<K))

  答案即为(1ll<<(K - 1) - 1)

  注意,我们需要特判一下n = 1的情况(显然!log(0)是不存在的!)

  代码:

点击查看代码
#include 
#include  
using namespace std;
typedef long long ll;
int main(){
    int t;
    scanf("%d",&t);
    while(t--)
    {
        ll n;
        cin>>n;
        if(n == 1)
        {
            printf("0\n");
            continue;
        }
        n = log(n - 1)/log(2) + 1;
        cout<<((1ll<<(n - 1)) - 1)<<endl;
    }
    return 0;
}

6954 Minimum spanning tree

签到题

    题意:给定一个数N,求2~N中所有点构成一颗最小生成树,使得其每条边的权值和最小(废话),每条边的权值定义为lcm(a,b)(a,b的最小公倍数),a,b分别为一条边的两个节点编号

   思路:仔细分析我们可以得到一下结论

   如果一个数是质数,那么与它连的边的权值至少是 这个质数*2

   如果一个数不是质数,那么与它连的边的权值至少是这个数

   比如一个数15,我们既然能够取到15这个数,那么也就必然存在3 5这两个结点,那么我们只需要将15连上3 or 5就能保证连了15这个节点的边最小了

   芜湖,这样我们就贪心得到了一颗最小生成树啦!

   方法:

   O(1e7)欧拉筛筛质数

   O(1e7)打表存ans数组

    O(q)输出询问 

   代码:

    

点击查看代码
#include <iostream>
#include <cmath> 
using namespace std;
typedef long long ll;
const int MAXN = 1e7 + 7;
int prime[MAXN];
int p[MAXN],top = 0;
ll ans[MAXN];
void ini()
{
    for(int i = 2;i < MAXN;i++)
    {
        if(!prime[i])    p[++top] = i;
        for(int j = 1;j <= top && p[j]*i < MAXN;j++)
        {
            prime[p[j]*i] = 1;
            if(i%p[j] == 0)    break; 
        }
    }
    
    ans[2] = 0;
    for(int i=3;i < MAXN;i++){
        if(!prime[i])    ans[i] = ans[i - 1] + i*2;
        else    ans[i] = ans[i - 1] + i;
    }
}
int main(){
    int t;
    scanf("%d",&t);
    ini();
    while(t--)
    {
        ll n;
        cin>>n;
        cout<<ans[n]<<endl;
    }
    return 0;
} 

6955 Xor sum

字典树

    题意:给定一段长为N的序列,求其中一段最小的子序列,使得这段子序列的异或和大于给定的K。输出子序列的左右端点。

    思路:

    首先这道题肯定不能O(n^2)

    问题转化:由于xor具有自反性,那么对于一段异或前缀和s[j]和s[i],s[i]^s[j] = a[i +1]^a[i + 2]^... ^a[j];

    so我们就能将问题转化为求一串序列中,寻找间隔最小的两个数,使得两个数的异或值大于给定的K。输出这两个数的下标。

    考虑字典树,枚举每一个右端点,这个端点之前的所有数已加入到字典树中。

    我们贪心地去寻找与当前右端点的数A异或最大的那个在它左边的数,返回下标,如果这个最大值大于K的话更新答案。

    这里说下为什么每次贪心找异或最大的那个数(其实只要大于K都行)就能保证是最优的:

    似乎是构造这样一个测试点很困难(我也没构造...本蒟蒻也不会证QAQ,队友过了我都是蒙的...)

    代码:

点击查看代码
#include <iostream>
#include <cstring>
#include <cmath> 
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
const int inf = 1e9 + 7;
int t[MAXN*31][3],tot = 0;
int a[MAXN];
void ins(int x,int idx)
{
    int p = 0;
    for(int i = 30;i >= 0;i--)
    {
        int u = x >> i & 1;
        if(!t[p][u])    t[p][u] = ++tot;
        p = t[p][u];
    }
    t[p][2] = idx;
}
int Go(int x)
{
    int p = 0;
    for(int i = 30;i >= 0;i--)
    {
        int u = x >> i & 1;
        if(t[p][!u])    p = t[p][!u];
        else    p = t[p][u];
        if(!p)
        return -1;//说明不存在 
    }
    return t[p][2];
}
int main(){
    int tt;
    scanf("%d",&tt);
    while(tt--)
    {
        for(int i = 0;i <= tot;i++)
        t[i][0] = t[i][1] = t[i][2] = 0;
        tot = 0;
        int n,k;
        scanf("%d %d",&n,&k);
        int ls = 0;
        int le,ri;
        int ans = inf;
        for(int i = 1;i <= n;i++)
        {
            int x;
            scanf("%d",&x);
            ls ^= x;
            a[i] = ls;
            int now = Go(ls);
            
            if((a[now]^ls) >= k)
            {
                if(i - now < ans)
                {
                    ans = i - now;
                    le = now + 1;
                    ri = i;
                }
            }
            ins(ls,i);
        }
        if(ans == inf)
        puts("-1");
        else
        printf("%d %d\n",le,ri);
    }
    return 0;
} 

     这里有个小技巧,就是每次清空字典树。如果我们直接调用memset对所有的节点清空的话,每次都会花费31*1e5,有些耗时。

     其实我们每次只要清空之前使用过的那个节点就行。

6957 Maximal submatrix

单调栈

     题意:给定一个矩阵,求一个最大的子矩阵(最大面积),使得子矩阵中每列元素都是递增的......

     思路:

     如果我们把输入的那个矩阵按照 列的递增个数前缀和 来表示的话,会得到差不多这样的一个矩阵:

    原矩阵:

    1 2 4

    2 3 3

     列递增前缀和矩阵(我自己取得名字方便描述)

    1 1 1

    2 2 1

    我们会发现对于每一行会记录下之前行递增的个数,这有利于我们求面积!

    我们考虑一下再这一行的数字的递增性质,会发现和单调栈的模板题非常像!(其实就是...)

    我们直接类比过来即可...

    于是问题转化为枚举每一行,求这一行能够构成的最大子矩阵面积,然后再更新最终的答案。

    代码:

    

点击查看代码
#include <iostream>
using namespace std;
const int MAXN = 2e3 + 7;
int n,m;
int a[MAXN][MAXN];
int sum[MAXN][MAXN];
int stk[MAXN],top = 0;
int w[MAXN];
int now[MAXN];
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d %d",&n,&m);
        for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
        scanf("%d",&a[i][j]);
        
        for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
        {
            if(a[i][j] >= a[i - 1][j])
            sum[i][j] = sum[i - 1][j] + 1;
            else
            sum[i][j] = 1;
        }
        
        int res =  0;
        for(int i = 1;i <= n;i++)
        {
            top = 0;
            for(int j = 1;j <= m;j++)
            now[j] = sum[i][j];
            
            int ans = 0;
            for(int j = 1;j <= m;j++)
            {
                int x = now[j];
                if(top == 0 || stk[top] <= x)
                stk[++top] = x,w[top] = 1;
                else
                {
                    int wid = 0;
                    while(top && stk[top] > x)
                    {
                        wid += w[top];
                        ans = max(ans,wid*stk[top]);
                        top -= 1;
                    }
                    stk[++top] = x;
                    w[top] = wid + 1;
                }
            }
            int wid = 0;
            while(top){
                wid += w[top];
                ans = max(ans,stk[top]*wid);
                top -= 1;
            }
            res = max(ans,res);
        }
        printf("%d\n",res);
    }
    return 0;
}

6958 KD-Graph

贪心+并查集

     题意:给定一个图,求一个长度D,使得这个图可以分成K份(割出点分成每一份),满足每一份之中的两两之间的距离 <= D,而份之间距离要 > D(当然也可以两个点不可达)

    思路:

    由于需要将所有的点进行分份,然后满足一定条件的边的权值太会加入到图中,于是我们可以从大到小枚举每一条边,然后每次将当前权值相同的所有边加入图中,如果某一次加完当前阶段的所有边(权值相同的边作为一个合并阶段),图中的点恰好分成了K份,那么这个边的权值就是答案!

    感性证明:

    在这个D之前的所有边都会加入到图中,使得我们的图中恰好存在K块,使得块与块之间不可达,而块内相互可达!这是毫无疑问的。

    而后我们加入剩下的所有边,这些边不会影响块与块之间的可达性(因为大于D,使得我们可以不承认块连接起来了!)

    证毕。

    代码:

     

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 500005;
struct node
{
    int x,y,w;
}a[MAXN];
int n,m,k;
bool cmp(node a,node b)
{return a.w < b.w;}
int fa[MAXN/5];
void ini()
{
    int Max = MAXN/5;
    for(int i = 0;i < Max;i++)
    fa[i] = i;
}
int Find(int x)
{
    if(x == fa[x])    return x;
    return fa[x] = Find(fa[x]);
}
bool merge(int x,int y)
{
    x = Find(x);
    y = Find(y);
    if(x != y)
    {
        fa[x] = y;
        return true;
    }
    return false;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        ini();
        scanf("%d %d %d",&n,&m,&k);
        for(int i = 0;i < m;i++)
        scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].w);
        if(n == k)
        {
            puts("0");
            continue;
        }
        
        sort(a,a + m,cmp);
        int now = n;
        int ans = -1;
        for(int i = 0;i < m;)
        {
            int cur_w = a[i].w;
            while(i < m && cur_w == a[i].w)
            {
                int x = a[i].x;
                int y = a[i].y;
                if(merge(x,y))
                now -= 1;
                
                i += 1;
            }
            if(now == k)
            {
                ans = cur_w;
                break;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

6959 zoto

莫队

     赛后牢骚:

     !!!真彩!!!分明离线查询很容易就想到莫队的嘛!

     题意:给定一些二维左边上的点,他们的x左边从1~n,y轴坐标在[0,100000]范围内。给定一个查询矩阵(x0,y0) (x1,y1)求在这个矩阵当中有多少个不同的y值大小不同的点。

     思路:

     离线查询->莫队

     我们先求出第一次查询的结果,然后根据第一次查询结果可以得到相邻区间查询的结果,优雅的暴力~

     这里只说下怎么由一个区间到另外一个区间...这里是二维的点啊喂!

     我们开一个数组num存放当前x0 - x1区间内所有的y值出现的次数(一个桶,num[y]表示y坐标出现的次数)

     然后我们对num进行分块 -> sum数组,每一块中保存其中不同y的个数

     这样对于每一次的区间x0 - x1的统计点,然后计算分布在y0 - y1就好啦!

     计算y0 - y1直接前缀和思想即可(什么,你不知道?就是计算0 ~ y1满足条件的y的个数 - 0 ~ y0 - 1中满足条件的y的个数)

     区间转移:
     相信你很容易就明白下面这两个函数:

void add(int x)
{
    num[fx[x]] += 1;
    if(num[fx[x]] == 1)    sum[fx[x]/K] += 1;
}
void del(int x)
{
    num[fx[x]] -= 1;
    if(num[fx[x]] == 0)    sum[fx[x]/K] -= 1; 
}

     add函数,将当前x轴上的点加入到num,sum数组中

     num就不说啦,看sum,由于我们只统计不重复的,所以统计第一次出现的那个y就好了,然后分块,分块大小K = sqrt(n)

     del函数也是一样的道理的啦!

     然后是计算0~y有多少个不重复的y:

int cal(int y)
{
    int ans = 0;
    int R = y/K;
    for(int i = 0;i < R;i++)    ans += sum[i];
    for(int i = R*K;i <= y;i++)    ans += (num[i] >= 1);
    return ans;     
}

      块内直接统计sum,离散部分暴力统计即可~

     完整代码:

     

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAXN = 100005;
int K;
struct node
{
    int l,r;
    int u,d;
    int idx;
    bool operator < (const node &a)const
    {
        if(l/K != a.l/K)    return l < a.l;
        if((l/K)&1)    return r > a.r;
        return r < a.r;    
    }
}a[MAXN];
int num[MAXN];//统计对应y下的x的个数 
int sum[MAXN];//分块中的各个值 
int ans[MAXN];
int fx[MAXN];
void add(int x)
{
    num[fx[x]] += 1;
    if(num[fx[x]] == 1)    sum[fx[x]/K] += 1;
}
void del(int x)
{
    num[fx[x]] -= 1;
    if(num[fx[x]] == 0)    sum[fx[x]/K] -= 1; 
}
int cal(int y)
{
    int ans = 0;
    int R = y/K;
    for(int i = 0;i < R;i++)    ans += sum[i];
    for(int i = R*K;i <= y;i++)    ans += (num[i] >= 1);
    return ans;     
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        memset(num,0,sizeof num);
        memset(sum,0,sizeof sum);
        int n,m;
        scanf("%d %d",&n,&m);
        for(int i = 1;i <= n;i++)    scanf("%d",&fx[i]);
        K = sqrt(n + 0.5);
        
        for(int i = 0;i < m;i++)
        scanf("%d %d %d %d",&a[i].l,&a[i].d,&a[i].r,&a[i].u),a[i].idx = i;
        
        sort(a,a + m);
        int le,ri;//先暴力算第一个 
        le = a[0].l,ri = a[0].r;
        for(int i = le;i <= ri;i++)    add(i);
        int now = cal(a[0].u) - cal(a[0].d - 1);
        ans[a[0].idx] = now;
        for(int i = 1;i < m;i++)
        {
            while(le < a[i].l)
            del(le++);
            while(le > a[i].l)
            add(--le);
             
            while(ri < a[i].r)
            add(++ri);
            while(ri > a[i].r)
            del(ri--);
            
            now = cal(a[i].u) - cal(a[i].d - 1);
            ans[a[i].idx] = now;
        }
        for(int i = 0;i < m;i++)
        printf("%d\n",ans[i]);
    }
    return 0;
}

未完待续....(其实是太菜了,其他的题还不会)

知识点预告:高斯消元、KD-Tree、数学公式QAQs

 

posted @ 2021-07-21 15:42  K0njac  阅读(412)  评论(2编辑  收藏  举报