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