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);
}
}