2021“MINIEYE杯”中国大学生算法设计超级联赛(4)部分题解
文章目录
A.Calculus
-  题意 
 给你函数集合 F F F,求 S ( x ) = ∑ i = 1 n ∑ j = 1 x f i ( j ) S(x)=\sum_{i=1}^n\sum_{j=1}^xf_i(j) S(x)=∑i=1n∑j=1xfi(j),求给定的 S ( x ) S(x) S(x)是否收敛。其中 n n n代表 S ( x ) S(x) S(x)中这些函数的个数, f i ( x ) f_i(x) fi(x)代表以自变量为 x x x的第 i i i个函数。
-  解题思路 
 可以发现,第二项实际上就是在求级数,若要使级数收敛,则 f i ( x ) f_i(x) fi(x)必须为 0 0 0,由此可得,每个函数的系数都必须是 0 0 0。
-  AC代码 
/**
  *@filename:Calculus
  *@author: pursuit
  *@csdn:unique_pursuit
  *@email: 2825841950@qq.com
  *@created: 2021-07-29 12:07
**/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100000 + 5;
const int P = 1e9+7;
int t;
string s;
void solve(){
    int cnt0 = 0,cnt = 0;
    for(int i = 0; i < s.size(); ++ i){
        if(isdigit(s[i])){
            cnt++;
        }
        if(s[i] == '0'){
            cnt0 ++;
        }
    }
    if(cnt == cnt0){
        cout << "YES" << endl;
    }
    else{
        cout << "NO" << endl;
    }
}
int main(){
    cin >> t;
    while(t -- ){
        cin >> s;
        solve();
    }
    return 0;
}
B.Kanade Loves Maze Designing
-  题意 
 给你一个 n n n个顶点的生成树,其中每个顶点都有一个点权 c i c_i ci,其中 p ( u , v ) p(u,v) p(u,v)代表 u − > v u->v u−>v的路径上有多少个不同的点权数量。定义 f ( i , x ) = ∑ j = 1 n p ( i , j ) × x j − 1 f(i,x)=\sum_{j=1}^np(i,j)\times x^{j-1} f(i,x)=∑j=1np(i,j)×xj−1,求出 i ∈ [ 1 , n ] , f ( i , P 1 ) , f ( i , P 2 ) i\in [1,n],f(i,P1),f(i,P2) i∈[1,n],f(i,P1),f(i,P2)的值。
-  解题思路 
 首先,我们必然是要从每个点出发求计算出到每个点中的 p ( u , v ) p(u,v) p(u,v),这个我们可以通过 d f s dfs dfs实现,用 c n t cnt cnt数组存储,使用 + − 1 +-1 +−1法来计算。存储好之后就可以开始累加了,注意预处理 x j − 1 x^{j-1} xj−1。
-  AC代码 
/**
  *@filename:Kanade_Loves_Maze_Designing
  *@author: pursuit
  *@created: 2021-08-04 14:13
**/
#include <bits/stdc++.h>
#define debug(a) cout << (#a)<< ":" << a << endl;
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 2e3 + 10;
const int X = 19560929;
const int P1 = 1e9 + 7;
const int P2 = 1e9 + 9;
const int INF = 0x3f3f3f3f;
int t,n;
int f1[N],f2[N];
int c[N],cnt[N],p[N];//点权,出现次数,p(i,j)。
int ans;
vector<int> g[N];//边权图
void init(){
    f1[1] = f2[1] = 1;
    for(int i = 2; i < N; ++ i){
        f1[i] = 1LL * f1[i - 1] * X % P1;
        f2[i] = 1LL * f2[i - 1] * X % P2;
    }
}
void dfs(int u,int fu){
    if(!cnt[c[u]]){
        ans ++;
    }
    cnt[c[u]] ++;
    p[u] = ans;
    for(auto &v : g[u]){
        if(v != fu){
            dfs(v,u);
        }
    }
    if(cnt[c[u]] == 1){
        ans --;
    }
    cnt[c[u]] --;
}
void solve(){
    for(int i = 1; i <= n; ++ i){
        ans = 0;
        dfs(i,-1);
        int h1 = 0,h2 = 0;
        for(int j = 1; j <= n; ++ j){
            h1 = (h1 + 1LL * f1[j] * p[j]) % P1;
            h2 = (h2 + 1LL * f2[j] * p[j]) % P2;
        }
        printf("%d %d\n", h1, h2);
    }
    for(int i = 1; i <= n; ++ i){
        g[i].clear();
    }
}
int main(){	
    scanf("%d", &t);
    init();
    while(t -- ){
        scanf("%d", &n);
        int x;
        for(int i = 2; i <= n; ++ i){
            scanf("%d", &x);
            g[x].push_back(i),g[i].push_back(x);
        }
        for(int i = 1; i <= n; ++ i){
            scanf("%d", &c[i]);
        }
        solve();
    }
    return 0;
}
D.Display Substring
-  题意 
 给你一个字符串和每个小写字母的消耗,那么一个字符串的和即是该字符串中所有字符的消耗总和。问这第 k k k个最小能量消耗的字串消耗是多少?
-  解题思路 
 由于这道题需要子字符串,而我们知道,如果一个子字符串消耗必定比原字符串消耗要少,我们可以处理前缀,而我们知道所有的后缀自然也可得到所有的前缀。处理出后缀数组,然后我们遍历后缀数组,对于每个后缀,其越长的前缀能耗越大,于是可以二分找到能耗小于等于要 check 的值的前缀的个数,再减去重复部分即可。而每个后缀被重复统计的部分就是 height 数组对应的值。
-  AC代码 
/**
  *@filename:后缀数组模板
  *@author: pursuit
  *@created: 2021-08-05 11:16
**/
#include <bits/stdc++.h>
#define debug(a) cout << (#a)<< ":" << a << endl;
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 1e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
int t,n;
ll k;
int c[27],sum[N];
char s[N];
int buf1[N], buf2[N], buc[N], sa[N], rnk[N], height[N];
//sa[i]表示排第i的是suffix(sa[i]),rnk[i]表示suffix(i)在所有后缀中排第几。
//height[i]表示后缀sa[i - 1]和后缀sa[i]的最长公共前缀。height数组表示排名相邻的两个后缀的最长公共前缀。
void suffix_sort(){
    int *x = buf1, *y = buf2, m = 127; //m是基数排序的基数的最大值
    //buf1和buf2是两个用来缓存的临时数组,用指针定义方便循环使用
    /**** 接下来先对所有单个字符基数排序 ****/
    for(int i = 0; i <= m; ++ i) buc[i] = 0; //先把桶清空
    for(int i = 1; i <= n; ++ i) buc[x[i] = s[i]]++; //此时,x[i]是一个字符串的“值”
    for(int i = 1; i <= m; ++ i) buc[i] += buc[i - 1];
    for(int i = n; i; -- i) sa[buc[x[i]]--] = i;
    for(int k = 1; k <= n; k <<= 1){
        //倍增枚举所比较的字符串的长度(字符串的长度的一半是k,即x[i]中此时存储的信息来自i开头长为k的字符串)
	int p = 0; //p统计当前放入新桶的字符串的个数
        //由于上一轮排序后,sa数组中存储的顺序相当于这一轮的第二关键字排序结果
        //所以接下来把上一轮的排序结果转译成这一轮字符串的按第二关键字排序结果,把排好序的后缀放在y数组中
	for(int i = n - k + 1; i <= n; i++) y[++p] = i;
        //对于所有右半部分为空的字符串,它们的第二关键字最小,所以排在最前面
	for(int i = 1; i <= n; i++) if(sa[i] > k) y[++p] = sa[i] - k;
        //按顺序枚举上一轮排序后的sa数组中的字符串,如果它作为第二关键字对某个字符串有贡献
        //则将被贡献字符串放入y数组
        //至此,y数组中是1~n的后缀编号,已按第二关键字大小升序排列。
	for(int i = 0; i <= m; ++ i) buc[i] = 0; //再进行一次基数排序
	for(int i = 1; i <= n; ++ i) buc[x[y[i]]]++;
        //还记得x[i]存的是什么吗?是i开头长为k的字符串的“值”,即第一关键字
	for(int i = 1; i <= m; ++ i) buc[i] += buc[i - 1];
	for(int i = n; i; i--) sa[buc[x[y[i]]]--] = y[i];
        /**** 接下来更新一个字符串的“值”——这次是长为2k的字符串的“值”了 ****/
	swap(x, y), x[sa[1]] = p = 1;
        //注意这里交换了一下x和y……现在y是上一轮(长为k)的“值”,x是这一轮(长为2k)的“值”
	for(int i = 2; i <= n; i++)
	    if(y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k]) x[sa[i]] = p;
        //两个字符串的两个关键字完全相同,则新的“值”也相同
	    else x[sa[i]] = ++p; //否则它对应着一个新的“值”
	if((m = p) >= n) break; //如果每个后缀对应值都不一样,则已经排好序了
    }
    /**** 接下来记录rank数组 ****/
    for(int i = 1; i <= n; ++ i) rnk[sa[i]] = i;
    /**** 接下来处理height数组:sa[i]和sa[i - 1]对应字符串的LCP ****/
    for(int i = 1, j, k = 0; i <= n; ++ i){
	if(rnk[i] == 1) continue;
        if(k) k--; //h[i] >= h[i - 1] - 1,k记录上一个h[i]
        j = sa[rnk[i] - 1];
        while(s[i + k] == s[j + k] && i + k <= n && j + k <= n) k++;
        height[rnk[i]] = k;
    }
}
ll check(int x){
    int t = 1;
    ll ans = 0;
    //检索后缀。
    for(int i = 1; i <= n; ++ i){
        while(t <= n && sum[t] - sum[i - 1] <= x)
            ++t;
        int temp = (t - i - height[rnk[i]]);
        ans += temp > 0 ? temp : 0;
    }
    //返回满足的前缀个数。
    return ans;
}
void solve(){
    suffix_sort();//构建后缀数组。
    //越长的前缀消耗越大。我们可以二分答案,找到消耗小于等于check值的前缀的个数。即对后缀来说越大的即是。
    int l = 0,r = 10000001;
    while(l < r){
        int mid = (l + r) >> 1;
        if(check(mid) < k)
            l = mid + 1;
        else
            r = mid;
    }
    printf("%d\n",l == 10000001 ? -1 : l);
}
int main(){	
    scanf("%d", &t);
    while(t -- ){
        scanf("%d%lld", &n, &k);
        scanf("%s", s + 1);
        for(int i = 0; i < 26; ++ i){
            scanf("%d", &c[i]);
        }
        for(int i = 1; i <= n; ++ i){
            sum[i] = sum[i - 1] + c[s[i] - 'a'];//求前缀和。我们要找的也就是前缀字符串。
        }
        solve();
    }
    return 0;
} 
H.Lawn of the Dead
-  题意 
 有一个 n × m n\times m n×m的网格地图,其中有 k k k个地雷。作为僵尸,它的起点在 ( 1 , 1 ) (1,1) (1,1),其只能往下或往右走,当遇到有地雷的单元格是不能行走的,问能走的单元格数是多少?
-  解题思路 
 由于空间很大,但地雷数是非常有限的,而我们又可以发现,如果一个单元格不能走,那么必定在上方和左方都有地雷(我们默认边界就是地雷),并且该点就会对右方和下方的产生影响。那么我们可以将地雷分割的区间段找出来,即每段没有地雷的区间,我们找上一行这一段区间中最左边能到达的单元格,如果能到达,那么其该位置到右端点的区间都能到达。 所以我们可以利用线段树来维护区间信息,查找区间,由于要利用上一行的信息,得到当前行的信息,所以我们要利用两颗线段树交替使用,时间复杂度为 O ( l o g n ) O(logn) O(logn)。
-  AC代码 
/**
  *@filename:LawnOfTheDead
  *@author: pursuit
  *@created: 2021-08-04 14:48
**/
#include <bits/stdc++.h>
#define debug(a) cout << (#a)<< ":" << a << endl;
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 1e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
struct node{
    int l,r;
    int sum;//该区间中可以到达的区间点数。
    int flag;//初始化为-1,0表示不可到达,1表示可以到达。
}sTree[2][N << 4];
vector<int> g[N];
int t,n,m,k,x,y;//n * m grids,k个不可行的点。
void buildTree(int id,int rt,int l,int r){
    sTree[id][rt].l = l,sTree[id][rt].r = r;
    sTree[id][rt].sum = 0,sTree[id][rt].flag = -1;
    if(l == r){
        //说明到达叶子结点。
        return;
    }
    int mid = l + r >> 1;
    buildTree(id, rt << 1, l, mid);
    buildTree(id, rt << 1 | 1, mid + 1, r);
}
void pushdown(int id,int rt){
    if(sTree[id][rt].flag == -1){
        return;
    }
    sTree[id][rt << 1].sum = (sTree[id][rt << 1].r - sTree[id][rt << 1].l + 1) * sTree[id][rt].flag;
    sTree[id][rt << 1].flag = sTree[id][rt].flag;
    sTree[id][rt << 1 | 1].sum = (sTree[id][rt << 1 | 1].r - sTree[id][rt << 1 | 1].l + 1) * sTree[id][rt].flag;
    sTree[id][rt << 1 | 1].flag = sTree[id][rt].flag;
    //清除标记。
    sTree[id][rt].flag = -1;
}
void update(int id,int rt,int l,int r,int x){
    if(sTree[id][rt].l >= l && sTree[id][rt].r <= r){
        sTree[id][rt].sum = (sTree[id][rt].r - sTree[id][rt].l + 1) * x;
        sTree[id][rt].flag = x;
        return;
    }
    pushdown(id,rt);
    int mid = sTree[id][rt].l + sTree[id][rt].r >> 1;
    if(l <= mid){
        //说明在左子树。
        update(id,rt << 1,l,r,x);
    }
    if(r > mid){
        //说明在右子树。
        update(id,rt << 1 | 1,l,r,x);
    }
    sTree[id][rt].sum = sTree[id][rt << 1].sum + sTree[id][rt << 1 | 1].sum;
}
int query(int id,int rt,int l,int r){
    if(!sTree[id][rt].sum){
        //说明这段区间没有可走的点。
        return INF;
    }
    if(sTree[id][rt].l == sTree[id][rt].r){
        return sTree[id][rt].l;
    }
    pushdown(id,rt);
    //注意这里的细节,若左子树存在,右子树就可以不用去遍历了。
    if(sTree[id][rt].l >= l && sTree[id][rt].r <= r){
        if(sTree[id][rt << 1].sum > 0){
            //说明存在可选点。
            return query(id,rt << 1,l,r);
        }
        else{
            return query(id, rt << 1 | 1,l,r);
        }
    }
    int mid = sTree[id][rt].l + sTree[id][rt].r >> 1;
    int minn1 = INF,minn2 = INF;
    if(l <= mid){
        //说明在左子树。
        minn1 = query(id,rt << 1,l,r);
    }
    if(r > mid){
        //说明在右子树。
        minn2 = query(id, rt << 1 | 1,l,r);
    }
    return min(minn1,minn2);
}
void solve(){
    buildTree(0,1,1,m);
    buildTree(1,1,1,m);
    update(0,1,1,1,1);//设置第一行的第一个点能走。
    int l,r;
    ll ans = 0;
    for(int i = 1; i <= n; ++ i){
        g[i].push_back(0),g[i].push_back(m + 1);
        sort(g[i].begin(),g[i].end());
        for(int j = 0; j < g[i].size() - 1; ++ j){
            l = g[i][j], r = g[i][j + 1];
            if(l + 1 <= r - 1){
                //查询上一行这一段区间中早能到达的点。
                int minn = query((i & 1) ^ 1,1,l + 1,r - 1);
                if(minn != INF){
                    //说明存在可走的点。
                    update(i & 1,1,minn,r - 1,1);
                }
            }
        }
        ans += sTree[i & 1][1].sum;
        update((i & 1) ^ 1,1,1,m,0);//将其清空,供下次使用。
    }
    printf("%lld\n", ans);
    for(int i = 1; i <= n; ++ i){
        g[i].clear();
    }
}
int main(){	
    scanf("%d", &t);
    while(t -- ){
        scanf("%d%d%d", &n, &m, &k);
        while(k -- ){
            scanf("%d%d", &x, &y);
            g[x].push_back(y);
        }
        solve();
    }
    return 0;
}
I.License Plate Recognition
-  题意 
 给定一个像素图,找到每个单词、数字或者汉字的左端点和右端点的位置。
-  解题思路 
 由于我们只需要处理 y y y,所以我们可以将像素图投影到一维,然后处理每段连续的#即可。需要注意汉字有可能不连续,所以我们可以先处理完数字和字母。
-  AC代码 
/**
  *@filename:License_Plate_Recognition
  *@author: pursuit
  *@created: 2021-08-05 10:20
**/
#include <bits/stdc++.h>
#define debug(a) cout << (#a)<< ":" << a << endl;
#define l first
#define r second
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 33 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
int t;
char s[35][110];
bool vis[110];//判断该列有没有出现字符。
pii pos[N];
void solve(){
    for(int j = 1; j <= 100; ++ j){
        for(int i = 1; i <= 30; ++ i){
            if(s[i][j] == '#'){
                vis[j] = true;
                break;
            }
        }
    }
    int l, r = -1;
    int cnt = 7;
    for(int j = 99; j >= 0; -- j){
        if(vis[j]){
            if(r == -1){
                r = j;
            }
            if(cnt == 1){
                //此时记录了r的位置。
                break;
            }
            l = j;
        }
        else{
            if(r != -1){
                pos[cnt].l = l,pos[cnt].r = r;
                cnt --;
                r = -1;
            }
        }
    }
    for(int j = 0; j < 100; ++ j){
        if(vis[j]){
            l = j;
            pos[cnt].l = l,pos[cnt].r = r;
            break;
        }
    }
    for(int i = 1; i <= 7; ++ i){
        printf("%d %d\n", pos[i].l + 1, pos[i].r + 1);
    }
    memset(vis,false,sizeof(vis));
}
int main(){	
    scanf("%d", &t);
    for(int Case = 1; Case <= t; ++ Case){
        for(int i = 0; i < 30; ++ i){
            scanf("%s", &s[i]);
        }
        printf("Case #%d:\n",Case);
        solve();
    }
    return 0;
}

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号