2021“MINIEYE杯”中国大学生算法设计超级联赛 第四场题解
第一题傻瓜题卡了两小时?
6986 Kanade Loves Maze Designing
题意:
给定一棵树,树上每个点的编号从1 - N,然后每个点有一个值。
现在要你从树上每一个点出发,到其他点,然后带入到该函数中求函数值......
有点绕,我们打个比方:
比如现在我们从第 i 个点出发,显然轮到遍历第 j 个点了,函数中 ai,j 代表的就是从i -> j我能拿去不同权值的数量
比如下面这样一棵树
其中:
1 -> 1
2 -> 1
3 -> 4
4 -> 5
5 -> 1
6 -> 4
以上是各个点的权值
那么我们从1 -> 2的ai,j就是1,因为只能拿到1这个种类的数
但是我们从1 -> 4的ai,j就是2,因为我们能拿到1和5这两个数
知道了这个ai,j是怎么回事,下一步就是怎么算?
由于是从一个点出发,我们需要遍历整个图,然后对于一棵树我们可以知道如下性质:
每两点之间的路径有且只有一条(如果不重复经过一条边的话)
那么其实我们从一个节点出发,遍历一遍这个树,就能得到ai,j的值了
具体DFS代码如下:
点击查看代码
void dfs(int cur,int fa)
{
if(!cn[c[cur]])
now[cur] = now[fa] + 1;
else
now[cur] = now[fa];
cn[c[cur]] += 1;
for(int i = head[cur];i;i = a[i].ne)
{
int to = a[i].to;
if(to != fa)
dfs(to,cur);
}
cn[c[cur]] -= 1;
}
简单解释一下:
从cur点出发,如果当前点的权值是第一次遍历得到,那么就相当于是在父亲结点的a i , j基础上+1
否则就是父亲结点的值
然后记录一下这个权值。
后面我们退出当前节点时需要将此权值回溯,也就是 - 1操作~
完整代码:
点击查看代码
点击查看代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXN = 2000 + 7;
const int MOD1 = 1e9 + 7;
const int MOD2 = 1e9 + 9;
typedef long long ll;
struct node
{
int ne,to;
int w;
}a[MAXN<<2];
int head[MAXN],cnt;
void add(int x,int y,int w = 0)
{
a[++cnt].ne = head[x];
head[x] = cnt;
a[cnt].to = y;
a[cnt].w = w;
}
ll c[MAXN];
ll pw1[MAXN],pw2[MAXN];
void ini()
{
pw1[0] = pw2[0] = 1;
for(int i = 1;i < MAXN;i++)
{
pw1[i] = pw1[i - 1]*19560929%MOD1;
pw2[i] = pw2[i - 1]*19560929%MOD2;
}
}
ll now[MAXN];
ll cn[MAXN];
void dfs(int cur,int fa)
{
if(!cn[c[cur]])
now[cur] = now[fa] + 1;
else
now[cur] = now[fa];
cn[c[cur]] += 1;
for(int i = head[cur];i;i = a[i].ne)
{
int to = a[i].to;
if(to != fa)
dfs(to,cur);
}
cn[c[cur]] -= 1;
}
int main()
{
int t;
ini();
// ll ans = 0;
// int b[6] = {1,1,2,2,1,2};
// for(int i = 0;i < 6;i++)
// {
// ans = ans + b[i]*pw1[i]%MOD1;
// ans %= MOD1;
// }
// cout<<ans;
// return 0;
scanf("%d",&t);
while(t--)
{
for(int i = 1;i <= cnt;i++)
head[i] = 0;
cnt = 0;
int n;
scanf("%d",&n);
for(int i = 2;i <= n;++i)
{
int x;
scanf("%d",&x);
add(x,i);
add(i,x);
}
for(int i = 1;i <= n;++i) scanf("%lld",&c[i]);
for(int i = 1;i <= n;++i)
{
now[i] = 0;
dfs(i,i);
ll ans1 = 0,ans2 = 0;
for(int j = 1;j <= n;++j)
{
ans1 = ans1 + now[j]*pw1[j - 1]%MOD1;
ans1 %= MOD1;
ans2 = ans2 + now[j]*pw2[j - 1]%MOD2;
ans2 %= MOD2;
}
printf("%lld %lld\n",ans1,ans2);
}
}
return 0;
}
6992 Lawn of the Dead
两场连续的比赛都有从1,1出发到N,M,而且每次都是往右或者往下走的这种题,还是不会,真彩
题意:
给你一个僵尸,开始在(1,1),只能往右走和往下走,然后有些地方有雷,问你僵尸可以访问的节点数量
思路:
首先我们非常容易得到一个结论,那就是
第一行出现的第一个禁区之后的所有点我们都不能走到
嗯,然后就没有然后了
后面的情况会有些复杂,dalao们觉得简单当我没说QAQ
我们思考一下这样一个问题:
对于一行一段连续的区间,什么情况下我们才能走这段区间的点呢?
看下面的图~
比如说我们现在判断下面一行的第二段区间:
我们可以很容易的发现只要上面一段区间与当前判断区间有交集,那么我就能从上面那段区间下来,访问下面区间上的点
换句话说,对于当前判断区间
找到上一行中与当前区间有交集的区间,有两种情况:
①找不到,那么这段区间访问不到
②找到了,左端点取一个最大值(与上一行的交集区间),然后右端点就是原判定区间的右端点,保存这段区间,以便进行下一行的判断(我愿称之为滚动模拟
这样的话,其实我们可以直接二分求得上一行区间左端点恰好大于当前判定区间左端点的那个区间(有点绕)
emmm..画个图:
可见左端点具有非常明显的单调性...然后我们就能轻易地二分找到啦!(这个图是后面那个区间
但是我们仔细观察会发现!其实上面这个图选取前面那个区间才是最好的!因为这样我就能遍历所有的当前区间了!
So,为了偷懒...我在程序里面找到恰好大于当前左端点的那个区间后,直接前后各找5个进行判断QAQ,这样能够保证不漏区间....
这样的话,我们就能得到这样一条思路:
①首先特判第一行,保存第一段可行区间
②每一行都保存这一行的地雷坐标(注意是按行保存),最后每行再加入一个m + 1列的地雷以便判断最后一段区间
③然后我们枚举行数,每次判断两个地雷之间的地雷即可
注意开long long QAQ
代码:
点击查看代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int MAXN = 1e5 + 7;
const int inf = 1e9 + 7;
struct node{
int le,ri;
node(int le = 0,int ri = 0):le(le),ri(ri){}
};
vector<int> a[MAXN];
vector<node> ok;//保存上一行的可行区间
bool intval(node a,int le,int ri)
{
if(a.le <= le)
return a.ri >= le;
else if(a.le <= ri)
return 1;
return 0;
}
int exist(int le,int ri)
{//先二分找做区间恰好小于le的,然后二分找恰好大于le的
int l = 0,r = ok.size() - 1;
int ans = r;
while(l <= r)
{
int mid = l + r >> 1;
if(ok[mid].le <= le)
l = mid + 1;
else
r = mid - 1,ans = r;
}
int res = inf;
int now = le;
for(int i = ans - 5;i <= ans + 5;i++)
{
if(i < 0 || i >= ok.size()) continue;
if(intval(ok[i],le,ri))
{
now = max(now,ok[i].le);
res = min(res,now);
}
}
return res == inf ? -1:res;
}
/*
1
100000 100000 3
1 3
3 1
3 3
*/
int main()
{
// freopen("1008.in","r",stdin);
// freopen("ans.out","w",stdout);
int t;
scanf("%d",&t);
while(t--)
{
int n,m,k;
scanf("%d %d %d",&n,&m,&k);
for(int i = 1;i <= n;i++)
a[i].clear();
for(int i = 0;i < k;i++)
{
int x,y;
scanf("%d %d",&x,&y);
a[x].push_back(y);
}
for(int i = 1;i <= n;i++)
{
a[i].push_back(m + 1);
sort(a[i].begin(),a[i].end());
}
ok.clear();
ok.push_back(node(1,a[1][0] - 1));
long long ans = 0;
ans += (ok[0].ri - ok[0].le + 1);
int ls = 1;//左区间
for(int i = 2;i <= n;i++)
{
vector<node> cur;
ls = 1;
for(int j = 0;j < a[i].size();j++)
{
int ne = a[i][j];
//现在主要就是求解ls - ne区间了
if(ls >= ne)
{
ls = ne + 1;
continue;
}
int le = exist(ls,ne - 1);//返回能走的左区间
if(le == -1)//说明不行
{
ls = ne + 1;
continue;
}
cur.push_back(node(le,ne - 1));
ls = ne + 1;
ans += ne - le;
}
ok = cur;
}
printf("%lld\n",ans);
}
return 0;
}
6988 Display Substring
属于是后缀自动机给自己搞傻了
题意:
给你一个只包含小写字符字符串,每个小写字符有一个固定的权值。
然后要你求出第K小的子串的权值是多少。
这个题定义串的权值就是该串中所有字符的权值之和。
思路:
考虑一下这样一个问题:
当权值越来越大的时候,是否小于此权值的字符串数量也在递增呢?
那是肯定的啦!因为权值是一个正整数!
于是,我们就能对权值的大小进行二分(其实就是二分答案。
然后我们再来考虑这个问题:
如何统计满足小于该权值的子串数目?
将此问题分为两个问题:
①如何统计子串?
②如何统计子串权值?
对于①,我们可以使用后缀数组(其中不同的子串的数目就是:
emm,为什么是这个就超出了本文章的业务范围了...读者可以自行学习有关《后缀数组》的相关内容
回到正文~
对于②,我们可以使用前缀和求整个串的前缀和。
对于从 sa[i] 开始的后缀,它的子串数目是
对于从sa[i]开始的字符串,我们可以二分地去查找满足小于该权值的最大长度。(这个显然是有一个单调性的,因为它也是一个正整数的前缀和)
比如说是 j
那么当前后缀字符串可行的子串数目就是 j - sa[i] + 1
很明显,我们需要拿这个数和height[i]比较,因为要去除重复项
说起来也简单,也就是和height[i]比较一下~
然后我们从1 - n遍历求出所有符合条件的子串数目,就完成了主函数的check函数~
代码:
点击查看代码
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1e5 + 7;
typedef long long ll;
char s[MAXN];
int sa[MAXN],rk[MAXN],temp[MAXN];
int height[MAXN];
int sum[MAXN];
int w[30];
ll n,m,k;
bool cmp_sa(int i,int j)
{
if(rk[i] != rk[j]) return rk[i] < rk[j];
int ri = i + k <= n ? rk[i + k]:-1;
int rj = j + k <= n ? rk[j + k]:-1;
return ri < rj;
}
void cal_sa()
{
for(int i = 1;i <= n;i++)
{
rk[i] = s[i];//按照权值进行排序
sa[i] = i;
}
for(k = 1;k <= n;k <<= 1)
{
sort(sa + 1,sa + n + 1,cmp_sa);
temp[sa[1]] = 1;
for(int j = 1;j < n;++j)
temp[sa[j + 1]] = temp[sa[j]] + (cmp_sa(sa[j],sa[j + 1])?1:0);
for(int j = 1;j <= n;++j)
rk[j] = temp[j];
}
}
void Get_Height()
{
int k = 0;
for(int i = 1;i <= n;++i) rk[sa[i]] = i;
for(int i = 1;i <= n;++i)
{
if(rk[i] == 1) continue;
if(k) k -= 1;
int j = sa[rk[i] - 1];
while(i + k <= n &&j + k <= n && s[j + k] == s[i + k]) k += 1;
height[rk[i]] = k;
}
}
ll check(ll now)
{//当前now消耗下的子串数目(可构成的)
ll cnt = 0;
for(int i = 1;i <= n;i++)
{
int le = sa[i],ri = n,ans = -1;
while(le <= ri)
{
int mid = le + ri >> 1;
int cur = sum[mid] - sum[sa[i] - 1];//计算一下当前串需要多少消耗
if(cur <= now)//更新坐标位置
le = mid + 1,ans = mid;
else
ri = mid - 1;
}
if(ans != -1)
{//说明此位置的能找到相关子串
int num = ans - sa[i] + 1;//当前字符串的个数
if(num < height[i])//说明和前面的完全重复了!
num = 0;
else//说明有部分没有重复
num -= height[i];
cnt += num;
}
}
return cnt;
}
int main()
{
// freopen("1004.in","r",stdin);
// freopen("ans.out","w",stdout);
int t;
scanf("%d",&t);
while(t--)
{
scanf("%lld %lld",&n,&m);
scanf(" %s",s + 1);
for(int i = 0;i < 26;++i) scanf("%d",&w[i]);
for(int i = 1;i <= n;i++) sum[i] = sum[i - 1] + w[s[i] - 'a'];
cal_sa();
Get_Height();
ll le = 0,ri = sum[n];
ll ans = ri;
while(le <= ri)
{
ll mid = le + ri >> 1;
ll cnt = check(mid);//获得当前消耗下的子串数目。
if(cnt >= m)//给的消耗太多了
ri = mid - 1,ans = mid;
else
le = mid + 1;
}
if(check(ans) < m)
printf("-1\n");
else
printf("%lld\n",ans);
}
}