25-暑期-来追梦noip-卷9 总结
开题顺序:A-D-C-B。
分配时间:A 10min D 10min C 30min B 30min 剩下的时间都是在思考
A
预估 100,实际 41。
二分 + 哈希即可。需要轻微卡常。
实现
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#pragma G++ optimize("Ofast,unroll-loops")
using namespace std;
const int N=2e5+5;
const ull BASE=13331;
int n;
ull H[N],P[N];
string s;
unordered_map<int,bool> mp;
ull get(int l,int r){
if(l>r)
return 0;
return H[r]-H[l-1]*P[r-l+1];
}
bool check(int x){
mp.clear();
for(int i=1;i+x-1<=n;i++){
int h=get(i,i+x-1);
if(mp[h])
return 1;
mp[h]=1;
}
return 0;
}
signed main(){
//freopen("T1.in","r",stdin);
//freopen("T1.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>s;
s='#'+s;
P[0]=1;
for(int i=1;i<=n;i++)
H[i]=H[i-1]*BASE+s[i],P[i]=P[i-1]*BASE;
int l=0,r=n+1;
while(l+1<r){
int mid=(l+r)>>1;
if(check(mid))
l=mid;
else
r=mid;
}
cout<<l;
return 0;
}
B
预估 41,实际 50。
现在我们需要求一个等差数列。
考虑变换一下形式:\(H_i,H_j,H_k \to H_j-d,H_j,H_j+d\),其中 \(d\) 为公差。
然后有一个很妙的操作:对于每个 \(i\),将 \(i\) 左边出现过的数标记为 \(1\),其他为 \(0\)。
这样,如果 \(H_j-d,H_j+d\) 不位于同一侧(即合法),则它们的标记串必定不回文。
然后问题转化为回文判定问题,且带修。于是运用线段树维护哈希值即可。
实现
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
const int N=3e5+5;
const ull BASE=13331;
int n;
int a[N];
ull tree1[N<<2],tree2[N<<2],P[N];
void pushup1(int p,int lt,int rt){
int mid=(lt+rt)>>1;
tree1[p]=tree1[p<<1]*P[rt-mid]+tree1[p<<1|1];
}
void upd1(int p,int lt,int rt,int qx,ull val){
if(lt>qx||rt<qx)
return;
if(lt==rt){
tree1[p]=val;
return;
}
int mid=(lt+rt)>>1;
upd1(p<<1,lt,mid,qx,val);
upd1(p<<1|1,mid+1,rt,qx,val);
pushup1(p,lt,rt);
}
ull qry1(int p,int lt,int rt,int ql,int qr){
if(lt>qr||rt<ql)
return 0;
if(ql<=lt&&rt<=qr)
return tree1[p];
int mid=(lt+rt)>>1;
if(qr<=mid)
return qry1(p<<1,lt,mid,ql,qr);
if(ql>mid)
return qry1(p<<1|1,mid+1,rt,ql,qr);
return qry1(p<<1,lt,mid,ql,qr)*P[min(rt-mid,qr-mid)]+qry1(p<<1|1,mid+1,rt,ql,qr);
}
void pushup2(int p,int lt,int rt){
int mid=(lt+rt)>>1;
tree2[p]=tree2[p<<1]+tree2[p<<1|1]*P[mid-lt+1];
}
void upd2(int p,int lt,int rt,int qx,ull val){
if(lt>qx||rt<qx)
return;
if(lt==rt){
tree2[p]=val;
return;
}
int mid=(lt+rt)>>1;
upd2(p<<1,lt,mid,qx,val);
upd2(p<<1|1,mid+1,rt,qx,val);
pushup2(p,lt,rt);
}
ull qry2(int p,int lt,int rt,int ql,int qr){
if(lt>qr||rt<ql)
return 0;
if(ql<=lt&&rt<=qr)
return tree2[p];
int mid=(lt+rt)>>1;
if(qr<=mid)
return qry2(p<<1,lt,mid,ql,qr);
if(ql>mid)
return qry2(p<<1|1,mid+1,rt,ql,qr);
return qry2(p<<1,lt,mid,ql,qr)+qry2(p<<1|1,mid+1,rt,ql,qr)*P[min(mid-lt+1,mid-ql+1)];
}
signed main(){
//freopen("T2.in","r",stdin);
//freopen("T2.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
P[0]=1;
for(int i=1;i<=n;i++)
cin>>a[i],P[i]=P[i-1]*BASE;
for(int i=1;i<=n;i++){
int len=min(a[i]-1,n-a[i]);
if(len>=1){
ull ans1=qry1(1,1,n,a[i]-len,a[i]-1);
ull ans2=qry2(1,1,n,a[i]+1,a[i]+len);
//cout<<ans1<<' '<<ans2<<'\n';
if(ans1!=ans2){
cout<<"YES";
return 0;
}
}
upd1(1,1,n,a[i],1);
upd2(1,1,n,a[i],1);
}
cout<<"NO";
return 0;
}
总结:
- 转化思想的运用(等差数列转化、数组的转化)。
C
预估 0,实际 0。
字典序的比较,因为前缀相同的都要跳过,所以想到字典树维护。
然后对于字典树分叉的地方,只要当前冠军节点向兄弟节点连边跑拓扑即可得到拓扑序,然后就能得到字典序。
这里需要注意一个点,就是前缀相同的不能算。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,M=48;
int n,tot;
string s[N];
int trie[N][M],cnt[N],in[M];
vector<char> ans;
vector<int> G[M];
void insert(string x){
int cur=0;
for(int i=0;i<x.size();i++){
if(!trie[cur][x[i]-'a'])
trie[cur][x[i]-'a']=++tot;
cur=trie[cur][x[i]-'a'];
}
cnt[cur]=1;
}
int search(string x){
int cur=0,res=0;
for(int i=0;i<x.size()-1;i++){
cur=trie[cur][x[i]-'a'];
res+=cnt[cur];
}
return res;
}
bool topo(){
queue<int> q;
for(int i=0;i<26;i++)
if(!in[i])
q.push(i);
while(!q.empty()){
int cur=q.front();
q.pop();
ans.push_back((char)(cur+'a'));
for(int i:G[cur]){
in[i]--;
if(!in[i])
q.push(i);
}
}
for(int i=0;i<26;i++)
if(in[i]>0)
return 0;
return 1;
}
bool check(string x){
for(int i=0;i<26;i++)
G[i].clear();
for(int i=0;i<26;i++)
in[i]=0;
int cur=0;
for(int i=0;i<x.size();i++){
for(int j=0;j<26;j++)
if(trie[cur][j]&&j!=(x[i]-'a'))
G[x[i]-'a'].push_back(j),in[j]++;
cur=trie[cur][x[i]-'a'];
}
return topo();
}
signed main(){
//freopen("T3.in","r",stdin);
//freopen("T3.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
cin>>s[i],insert(s[i]);
for(int i=1;i<=n;i++){
if(!search(s[i])&&check(s[i])){
for(char i:ans)
cout<<i;
cout<<'\n';
ans.clear();
}
else{
cout<<"nemoguce\n";
ans.clear();
}
}
return 0;
}
总结:
- 字典序想到字典树、拓扑排序。
D
预估 0,实际 0。
令 \(dp_{i,j}\) 表示前 \(i\) 个氨基酸,且匹配到碱基串的第 \(j\) 位的方案数。
初始 \(dp_{0,0}=1\),答案 \(\sum dp_{k,i}\)。
转移 \(dp_{i,j}=dp_{i,j}+dp_{i-1,j-len}\),其中 \(len\) 是第 \(k\) 个氨基酸选择的碱基序列。
此处转移因为要和 \(s\) 匹配,所以可以在 kmp 匹配上的时候转移。
实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int K=1e2+5,A=11,S=1e4+5,MOD=1e9+7;
int n,k,ans;
int dp[K][S],a[K];
string s,t[K][A];
int nxt[S];
void getnxt(string t){
int i=0,j=-1;
nxt[0]=-1;
for(;i<t.size();){
if(j==-1||t[i]==t[j])
i++,j++,nxt[i]=j;
else
j=nxt[j];
}
}
void kmp(string s,string t,int cur){
getnxt(t);
int i=0,j=0;
for(;i<s.size();){
if(j==t.size()-1&&s[i]==t[j]){
if(i>=t.size())
dp[cur][i]=(dp[cur][i]+dp[cur-1][i-t.size()])%MOD;
j=nxt[j];
}
if(j==-1||s[i]==t[j])
i++,j++;
else
j=nxt[j];
}
}
signed main(){
cin>>k>>s,n=s.size(),s="#"+s;
for(int i=1;i<=k;i++){
cin>>a[i];
for(int j=1;j<=a[i];j++)
cin>>t[i][j];
}
for(int i=0;i<n;i++) dp[0][i]=1;
for(int i=1;i<=k;i++){
for(int u=1;u<=a[i];u++){
kmp(s,t[i][u],i);
}
}
for(int i=1;i<=n;i++) ans=(ans+dp[k][i])%MOD;
cout<<ans;
return 0;
}
结语
成绩:41+50+0+0=91。
问题:对于转化思想和字典序问题不熟悉,dp 很弱。
方案:多练习 dp,其他同上总结。

浙公网安备 33010602011771号