蛋糕
题目描述
输入格式
一行两个整数 n 和 P, 意义如题面所示。
输出格式
一行一个整数, 表示有多少种切法。
输入样例
【样例一输入】
6 1000000007
【样例二输入】
20 572541752
输出样例
【样例一输出】
14
【样例二输出】
266161148
题解(卡特兰数+线性筛+快速幂)
题目看上去像个
dp
,设
f[n]
为
n
边形的答案,选一个三角形分割,将问题划分成两边,则
我们知道卡特兰数的计算公式为
F[n]=Cn2n−Cn+12n=Cn2nn+1
。这样如果算出结果是
O(n)
的,但是
P
不是质数,所以求不了逆元。如果强行分解质因数就是其实怀疑数据范围是不是错了,为毛
n
那么大啊。考试时想到这里我就想不出来了(不可能考什么CRT吧,rt。。)。
其实线性筛是可以的,我们预先筛出
我们枚举每个质数,拿去筛掉所有数,求到所有数中有多少个这样的质因子,并记录在数组里。假设当前的素数是
2
,那么
这样的时间复杂度降下去了,由每个数质因子个数*总数变成了总质数个数*log级别的倍增计算。于是时间大概是 O(cnt∗logN+N) , cnt 是质数数量,大约有 N/ln(N)个 。
这种方法其实很常见,快速求出一段区间的质因数个数,用线性筛。这种方法其实就是原来是先枚举
i
,然后一个个算,现在直接枚举质因数,然后算区间内有几个。很像求约数个数和,切换枚举次序,将相同的累积一起算,而不是离散地去求和。无奈我记忆力不好,这些套路总是记不住多半是废了。这个计算方法有些类似黎曼积分和勒贝格积分的差别。
代码
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cmath>
#define MAXN 20000010
using namespace std;
typedef long long LL;
LL P, ans = 1LL;
int n, cnt, num[MAXN], prime[MAXN];
bool vis[MAXN];
void Da(){
for(int i = 2; i <= (n<<1); i++){
if(!vis[i]) prime[++cnt] = i;
for(int j = 1; j <= cnt && i*prime[j] <= (n<<1); j++){
vis[i*prime[j]] = true;
if(i % prime[j] == 0) break;
}
}
}
void add(int x, int v){
for(int i = 1; i <= cnt; i++)
for(LL j = prime[i]; j <= x; j *= prime[i])
num[i] += x / j * v;
}
LL Pow(LL x, int y){
LL res = 1LL;
while(y){
if(y & 1) res = res * x % P;
x = x * x % P;
y >>= 1;
}
return res;
}
int main(){
scanf("%d%lld", &n, &P);
n -= 2;
Da();
add(n<<1, 1);
add(n+1, -1);
add(n, -1);
for(int i = 1; i <= cnt; i++) ans = ans * Pow(prime[i], num[i]) % P;
printf("%lld", ans);
return 0;
}
解密
题目描述
输入格式
两行, 第一行为字符串 s1, 第二行为字符串 s2。
输出格式
一行两个整数, 表示所有敌方可能传递的信息的最短长度和最长长度; 如果没有, 那么输出两个-1
输入样例
【样例一输入】
apple
pepperoni
【样例二输入】
testsetses
teeptes
【样例三输入】
bidhan
roy
输出样例
【样例一输出】
2 2
【样例二输出】
3 3
【样例三输出】
1 -1
题解(kmp/后缀数组/SAM)
话说这题真是6,kmp正解不难想到,同时也是后缀数组的果题,大神们用SAM也是眨眨眼就切掉。还有,如果你什么都不会,直接上
不瞎BB了,直接讲两种方法(SAM老子不会)。kmp的话,就直接枚举s1的位置,以其为开头求一次Next,然后拿去和s1和s2匹配。一开始我想这样是不是就能知道枚举位置后的每个长度的前缀在s1和s2中的出现次数了呢?如果是的话不直接看看出现次数是不是都等于1就好了吗?答案是否定的。假如说有一个后缀aa,匹配aaa,长度为1的加1次,长度为2的加1次,长度为2的再加1次,没了。总共1加了1次,2加了2次。很明显,其实这样是算少了的。
但是题目有一个性质:只有出现1次的才能成为答案,而如果出现了,kmp一定会算到。于是我们只需看看是否出现了两次及以上就行了。那这样怎么做呢?如果一个串在另一个串中出现了至少两次的话呢,那它除了本身加了外,一定作为Next被加了,当然可能被当作Next的Next加了。举个栗子,如果aaa匹配成功了,而且是跳Next跳出来的,我们就可能要将a加1次,aa也加一次,Next那些往回跳啊跳都要加。但是我们只关心出现次数是否大于1,只需要往回加1个,因为前面的已经被加过了,如果出现次数大于1就至少是2了,大概是这个样子。我在BB些啥。所以每次就将k和Next[k]那里+1。这样就能保证时间
n2
了。如果每次往回跳到底,暴力也有挺多分吧。
后缀数组的话,就将s1,s2拼在一起,用特殊字符隔开,然后求sa和height。这题都不用用到二分+分块(也用不了),直接枚举某个后缀和匹配长度l,然后找与其的lcp>=l的后缀有几个是s1的,有几个是s2的就行了。我忘了height数组的性质,一开始不会实现,以为是 n3 ,后来后缀数组大神KsCla告诉我这样是 n2 的,因为枚举后缀后,lcp向两边肯定不增,因为lcp是height的最小值,而height的最小值不增(废话),所以每次向上下扫,然后统计满足要求的s1,s2数量,一旦超过1就退出,所以最多向上和下走3步,走的步数是个常数。时间就是 O(n2+nlogn) 。
代码(kmp)
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <cmath>
#define MAXN 5005
using namespace std;
int times1[MAXN], times2[MAXN];
int Next[MAXN], len1, len2, ans1 = -1, ans2 = -1;
char s1[MAXN], s2[MAXN];
void Kmp(int l){
Next[l] = Next[l+1] = l;
int k = l;
for(int i = l+2; i <= len1; i++){
while(k != l && s1[i-1] != s1[k]) k = Next[k];
if(s1[i-1] == s1[k]) k ++;
Next[i] = k;
}
k = l;
for(int i = 1; i <= len1; i++){
while(k != l && s1[i-1] != s1[k]) k = Next[k];
if(s1[i-1] == s1[k]) k ++;
if(k){
times1[k-l] ++;
times1[Next[k]-l] ++;
}
if(k+l == len1) k = Next[k];
}
k = l;
for(int i = 1; i <= len2; i++){
while(k != l && s2[i-1] != s1[k]) k = Next[k];
if(s2[i-1] == s1[k]) k ++;
if(k){
times2[k-l] ++;
times2[Next[k]-l] ++;
}
if(k+l == len1) k = Next[k];
}
}
int main(){
scanf("%s%s", &s1, &s2);
len1 = strlen(s1);
len2 = strlen(s2);
for(int i = 0; i < len1; i++){
memset(times1, 0, sizeof(times1));
memset(times2, 0, sizeof(times2));
Kmp(i);
for(int j = 1; j <= len1; j++)
if(times1[j] == 1 && times2[j] == 1){
ans1 = max(ans1, j);
ans2 = (ans2 == -1) ? j : min(ans2, j);
}
}
printf("%d %d\n", ans2, ans1);
return 0;
}
代码(SA)
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#define MAXN 10010
using namespace std;
char s[MAXN], s1[MAXN], s2[MAXN];
int cnt[MAXN], tmp1[MAXN], tmp2[MAXN], sa[MAXN], height[MAXN];
int *x = tmp1, *y = tmp2, ans1 = -1, ans2 = -1;
int n, m = 1000;
bool Cmp(int *r, int a, int b, int l){
return r[a] == r[b] && r[a + l] == r[b + l];
}
void Da(){
int i, j, p;
for(i = 0; i < m; i++) cnt[i] = 0;
for(i = 0; i < n; i++) cnt[x[i] = s[i]] ++;
for(i = 1; i < m; i++) cnt[i] += cnt[i-1];
for(i = n-1; i >= 0; i--) sa[--cnt[x[i]]] = i;
for(j = 1, p = 1; p < n; j <<= 1, m = p){
for(p = 0, i = n-j; i < n; i++) y[p++] = i;
for(i = 0; i < n; i++) if(sa[i] >= j) y[p++] = sa[i] - j;
for(i = 0; i < m; i++) cnt[i] = 0;
for(i = 0; i < n; i++) cnt[x[y[i]]] ++;
for(i = 1; i < m; i++) cnt[i] += cnt[i-1];
for(i = n-1; i >= 0; i--) sa[--cnt[x[y[i]]]] = y[i];
for(i = 1, p = 1, swap(x, y), x[sa[0]] = 0; i < n; i++)
x[sa[i]] = Cmp(y, sa[i], sa[i-1], j) ? p-1 : p++;
}
}
void Calc_height(){
int i, j, k = 0;
for(i = 0; i < n; i++){
if(x[i]){
for(k?k--:0, j = sa[x[i]-1]; s[i+k] == s[j+k]; k++);
height[x[i]] = k;
}
else height[x[i]] = 0;
}
}
int main(){
scanf("%s%s", &s1, &s2);
int len1 = strlen(s1), len2 = strlen(s2);
for(int i = 0; i < len1; i++) s[n++] = s1[i];
s[n++] = '$';
for(int i = 0; i < len2; i++) s[n++] = s2[i];
s[n++] = char(0);
Da();
Calc_height();
for(int i = 0; i < n; i++){
for(int s = 1; s <= n-sa[i]; s++){
int Min = 1e6, cnt1 = 0, cnt2 = 0;
if(sa[i] < len1) cnt1 ++;
else cnt2 ++;
for(int l = i-1; l+1 < n && l >= 0; l--){
Min = min(Min, height[l+1]);
if(Min < s || cnt1 > 1 || cnt2 > 1) break;
if(sa[l] < len1) cnt1 ++;
else cnt2 ++;
}
Min = 1e6;
for(int r = i+1; r < n; r++){
Min = min(Min, height[r]);
if(Min < s || cnt1 > 1 || cnt2 > 1) break;
if(sa[r] < len1) cnt1 ++;
else cnt2 ++;
}
if(cnt1 == 1 && cnt2 == 1){
ans1 = max(ans1, s);
ans2 = ans2 == -1 ? s : min(ans2, s);
}
}
}
printf("%d %d\n", ans2, ans1);
return 0;
}
漏洞
题目描述
输入格式
输出格式
输入样例
【样例一输入】
5 5
38 43 4 12 70
1 1 3 4 8
2 2 4
1 4 5 0 8
1 2 5 8 7
2 1 5
【样例二输入】
5 5
25 36 39 40 899
1 1 3 2 7
2 1 2
1 3 5 9 1
1 4 4 0 9
2 1 5
输出样例
【样例一输出】
103
207
【样例二输出】
111
1002
题解(线段树)
本题是线段树的果题,我一看到题目就以为自己会了,结果爆零了(代码一堆漏洞)。
线段树上每个节点开一个数组num[]记此区间内的每个数的权值和。假设某个区间有两个数124和42,那么对应的num[1]=100,num[2]=11,num[4]=11。然后我们build()出这个信息,然后顺便求和sum。
然后我们先看简单的query(),明显是询问一段区间的和。
然和看看update(),要求将一段区间的x改成y,一开始我想每一个点记一个二维bool数组change[x][y],代表将x改成y。然而这样空间很吃紧,时间也有100倍常数,那时naive的我以为已经没什么其他好害怕的了,然而这样合并标记,就是down()的时候,无法知道先后顺序,假如是将x改成y,y改成z,我先改那个呢?于是我就光荣爆0。我们可以开一个链表链一下,然而这样时间还是100倍,非常蠢。其实我们多维护的懒惰标记是可以使常数变为10的。
我们考虑记mark[]代表这个区间内的所有的i被改成了mark[i],这样标记是可以合并的,而且下面可以说明常数为10。不考虑down(),先处理update(),现在区间[L, R]又来了一个标记是将u改成v,我们要更新答案sum,num[],以及懒惰标记mark[]。一开始的所有的mark[i]=i,现在mark[i]=j。如果u=j的话,代表现在我们要合并mark[i]=j和mark[j]=v。于是我们直接将mark[i]=v,更新好了mark[]。由于之前已经将mark[i]=i和mark[i]=j合并,答案已经算过。于是此时的贡献与更新就是sum+=num[u]*(v-u),num[v]+=num[u],num[u]=0。
于是update()我们也搞定了,就是说我们处理好了一个点的先后标记的合并与答案的更新,而且常数是10。
换到down(),我们需要处理父亲的标记与儿子标记的合并与儿子答案的更新。注意这时我们已经处理好父亲标记的先后顺序了,就是说能合并的已经在update()的时候或上一次down()搞定了。举个栗子,如果父亲有标记1->3,3->5,明显是先3->5再1->3的,所以我们不能直接看儿子的3然后改成5,因为儿子之前可能因为父亲的标记1->3而使3多了,这样标记就会冲突。所以我们开一个临时数组now[]保存儿子的num[],将儿子的num[]清0。然后将要改的部分累记到儿子的num[]里去,个数转移时还是用now[],这样最后剩下的那些now[]就再累加回儿子的num[]里去,常数也是10,sum也是用now[]去更新。mark[]的话向update()时一样改,最后记得清掉父亲的标记。
至此我们处理好了所有部分,时间复杂度为 O(10qlogN) 。
总结:线段数的题目就是在合并标记与更新答案,还要知道线段树里记些啥,这些信息要满足区间加法,要能够直接维护,如果不能就要借助其他的信息来维护。
不行,太严肃了,还是讲讲其他的吧。我发现线段树码错了后,最后一分钟将暴力交上去,结果暴力也爆零了,变量打错了QAQ。真是菜出强大。
代码
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <iostream>
#define MAXN 100010
using namespace std;
typedef long long LL;
int n, q;
LL a[MAXN];
struct Tnode{
LL sum, num[10];
int mark[10];
}tree[MAXN<<2];
void build(int root, int L, int R){
if(L == R){
LL temp = a[L];
int t = 1;
while(temp){
tree[root].num[temp%10] += t;
t *= 10;
temp /= 10;
}
tree[root].sum = a[L];
for(int i = 0; i <= 9; i++)
tree[root].mark[i] = i;
return;
}
int mid = (L + R) >> 1, Lson = root << 1, Rson = root << 1 | 1;
build(Lson, L, mid);
build(Rson, mid+1, R);
tree[root].sum = tree[Lson].sum + tree[Rson].sum;
for(int i = 0; i <= 9; i++)
tree[root].num[i] = tree[Lson].num[i] + tree[Rson].num[i];
for(int i = 0; i <= 9; i++)
tree[root].mark[i] = i;
}
void Down(int root, int Lson, int Rson){
LL now[10];
memcpy(now, tree[Lson].num, sizeof(now));
memset(tree[Lson].num, 0, sizeof(now));
for(int i = 0; i <= 9; i++){
int &temp = tree[Lson].mark[i];
if(tree[root].mark[temp] == temp) continue;
tree[Lson].sum += (tree[root].mark[temp] - temp) * now[temp];
tree[Lson].num[tree[root].mark[temp]] += now[temp];
now[temp] = 0LL;
temp = tree[root].mark[temp];
}
for(int i = 0; i <= 9; i++) tree[Lson].num[i] += now[i];
memcpy(now, tree[Rson].num, sizeof(now));
memset(tree[Rson].num, 0, sizeof(now));
for(int i = 0; i <= 9; i++){
int &temp = tree[Rson].mark[i];
if(tree[root].mark[temp] == temp) continue;
tree[Rson].sum += (tree[root].mark[temp] - temp) * now[temp];
tree[Rson].num[tree[root].mark[temp]] += now[temp];
now[temp] = 0LL;
temp = tree[root].mark[temp];
}
for(int i = 0; i <= 9; i++) tree[Rson].num[i] += now[i];
for(int i = 0; i <= 9; i++)
tree[root].mark[i] = i;
}
void update(int root, int L, int R, int x, int y, int u, int v){
if(x > R || y < L) return;
if(x <= L && y >= R){
tree[root].sum += (v - u) * tree[root].num[u];
tree[root].num[v] += tree[root].num[u];
tree[root].num[u] = 0LL;
for(int i = 0; i <= 9; i++)
if(tree[root].mark[i] == u) tree[root].mark[i] = v;
return;
}
int mid = (L + R) >> 1, Lson = root << 1, Rson = root << 1 | 1;
Down(root, Lson, Rson);
update(Lson, L, mid, x, y, u, v);
update(Rson, mid+1, R, x, y, u, v);
tree[root].sum = tree[Lson].sum + tree[Rson].sum;
for(int i = 0; i <= 9; i++)
tree[root].num[i] = tree[Lson].num[i] + tree[Rson].num[i];
}
LL query(int root, int L, int R, int x, int y){
if(x > R || y < L) return 0LL;
if(x <= L && y >= R) return tree[root].sum;
int mid = (L + R) >> 1, Lson = root << 1, Rson = root << 1 | 1;
Down(root, Lson, Rson);
LL temp1 = query(Lson, L, mid, x, y);
LL temp2 = query(Rson, mid+1, R, x, y);
return temp1 + temp2;
}
int main(){
scanf("%d%d", &n, &q);
for(int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
build(1, 1, n);
int op, l, r, x, y;
for(int i = 1; i <= q; i++){
scanf("%d%d%d", &op, &l, &r);
if(op == 1){
scanf("%d%d", &x, &y);
if(x == y) continue;
update(1, 1, n, l, r, x, y);
}
else
printf("%lld\n", query(1, 1, n, l, r));
}
return 0;
}
总结
这次考试的时间过得好快,时间分配不对,起码第二题还是能拿很多分的。第三题码不出,想的太少,没想好就码。最后暴力错了,真是不可饶恕。第一题数论不过关,快速分解质因数统计个数都不会,要总结经验,争取以后多拿点分。