前言:
我相信大家可以感觉到DAY2题目的难度明显比DAY1大很多,这也是近年NOIP考试的趋势,从目前NOIP考察的知识来看这次的T3知识可能对NOIP选手来说略难,但说不定今年NOIP还会考更高级的算法,所以要有所防备。
再说一点,大家一定要注意部分分的获取。对于这套题,如果能力一般的同学采用部分分算法,理论至少上可以得到:100(预处理+动态规划)+40(只解决纯串联或并联情况)+55(采用暴力的“逐步爬山法”计算)=195分,对于DAY2来说是一个不错的分数。对于T2,个人觉得细节相当多,而且不知道“标程”是否正确。对于T3,得部分分难度不大,但它就是一个防NOIP选手AK的题,可以说,对大部分人(包括我)来说这道题写正解(200+ 行代码)还不如暴力(约100行代码)来得快。
最后,不要认为出题人能力比其他人强。因为出题没有比赛那么短的时间限制,所以可以花更多时间来解决自己的题。比如对于T2的第一个和第二个版本我自己都做不来,于是改为了现在的第三个版本,而且“标程”写错两次;对于T3我自己写了大约3个小时才写完正解。所以这次比赛200+的同学都是能力在我之上的。 ——于2018/10/14
当然题目方法不唯一,如果大家对DAY2的题目有更好的解法,欢迎提出!
比赛链接:NOIP2018原创模拟赛DAY2
T1:最后的战役
考察知识:map,动态规划
算法难度:XXX+ 实现难度:XXX
说实话,我动态规划很弱,所以就出了一道不那么难的动态规划。
这道题还有更快的方法,请参考:洛谷 U40593 最后的战役(思维+离散化)
这道题贪心并不是完美解法,但是数据为随机生成的,所以得分概率非常大。
Hack数据:
4 2
1 1
2 3
3 6
4 8
1 2 3 4
标程输出:22 贪心输出:21
分析:
首先,我们要解决操作2,如果暴力枚举时间复杂度为。
其实吧,我们直接用map优化就可以了,时间复杂度:
for(int i=1;i<=n;i++) P[i]=max(P[i-1],p[i]);//求最大值
for(int i=1;i<=n;i++){
mp[k[i]]+=p[i];//在这里求和
P[i]=max(mp[x[i]],P[i]);
}
在处理了伏地魔在 [1,n] 每一步可以获得的最大魔法能量之后(记为P [ i ],记 为 SUM),我们就可以采取动态规划了:
定义:表示在[ 1 , i ] 秒中在 i 秒使用了魔法,且有 j 个时间段使用了魔法,可以得到的最大能量值
边界:
状态转移:
状态转移方程的实现还是要稍微处理一下,直接实现会超时:
for(int i=1;i<=n;i++)//处理边界
f[i][1]=SUM-P[i]+P[i+1],
ans=max(ans,f[i][1]);
for(int j=2;j<=m;j++){
for(int k=1;k<=n;k++) T[k]=max(T[k-1],f[k][j-1]);//预处理
for(int i=2*j-1;i<=n;i++)
f[i][j]=T[i-2]-P[i]+P[i+1],
ans=max(ans,f[i][j]);
}
好了,这道题我们就做出来了,理论时间复杂度:,但是时间效率不是很高,面对最大数据需要近900ms。
T2:流量计算
考察知识:图论,数学,推导,搜索
算法难度:XXXX 实现难度:XXXX
出题背景:我目前在学高中电学。最开始这道题搞得很难,我自己都做不来,于是不断降低难度:从混连->并联与并联和串联嵌套->并联只能嵌套串联。
分析:
这道题需要大家有一定的电学知识和数学推导能力。
先推导几个电学结论:
结论一:对于串联电路,我们可以将所有电阻看作一个等效电阻:
证明:易得,略
结论二:对于一个(等效)并联电路,电阻分别为等效电阻为:
证明:设电压为 U,由欧姆定律,总电流为:,故等效电阻为:
结论三:对于一个(等效)并联电路,电阻分别为,我们可以采取下面的算法计算等效电阻:
double R_=R[1];
for(int i=2;i<=n;i++) R_=R_*R[i]/(R_+R[i]);
ans=R_;
证明:结论二的二维形式:,每次将两个电阻合并为一个等效电阻,最后的值即为总等效电阻
结论四:对于一个(等效)并联电路,电阻分别为,其支路最小电流为:
证明:我们知道,最小电流在电阻最大的支路上,即,而电压为,由,就得到结论
有了这几个结论,我们就可以开始动手了:
1.先用结论三处理重边
2.然后bfs求出由电源正极到负极的一条路径
3.以这条路径为主线开始处理,遇到分叉边就表明此处有并联嵌套串联,用结论一+dfs计算嵌套串联的等效电阻,并记下,然后利用结论三进行并联电阻的合并
4.将所有等效电阻求和,即为整个电路的总等效电阻,如果主线电流为 则最小电流
代码实现细节请参考代码
T3:PION后缀自动机
考察知识:树链剖分,线段树,链表,排序,二分,字符串处理
算法难度:XXXXX 实现难度:XXXXX
分析:难题,算法复杂,代码量大!这是一道典型的考场上正解不如写暴力的题!
先介绍暴力算法(55分):
如果你的程序超时,你可以先尝试这道题:PION后缀自动机(数据弱化版)
算法难度:XXX 实现难度:XXX+
显然储存文件后缀信息不能用数组,空间开不下,所以我们用链表储存所有文件夹中文件的后缀信息;
然后是后缀的处理,注意到字符串长度小于6,如果我们用 1 表示 a ,2 表示 b ,... ... ,26 表示 z,这就相当于将字符串看做一个27进制数,这样我们就可以在 int 范围内用数字表示字符串了
解决了上面两个问题之后,我们就可以建树然后对每个操作用暴力的 “逐步爬山法” 解决了,实现难度不大
其实这种方法对随机数据效果还是比较好的(甚至比下面的满分算法还略快一点),但是你觉得数据可能完全随机么?
时间复杂度:极端情况约
满分算法:
代码量比较大。
看到这道题我们应该可以想到用树链剖分来做。
我们先考虑怎么对序列进行统计和修改,对于操作2,我们需要找到一个序列中的子序列。我们可以用一个结构体来储存,结构体储存序列中每个元素的值(字符串的hash值)和序列中每个元素在序列中的位置。然后将这个结构体序列按元素值的排序(元素值相同的按在序列中的位置排序)。当我们需要查找一个序列相同数值所在的区间的时候用 lower_bound 和 upper_bound 就可以完成,对于查找出来的目标区间,还需要用二分法来查找该区间中元素的下标在目标子序列中的数量。
至于修改,用线段树的区间维护实现。
解决了对序列的修改,我们还要解决树中每个节点含有的文件数目不相同的问题,如果一个节点没有文件,那么简单的方法是新建一个伪文件,可以将hash值赋为 -1。
之后就是树链剖分了,不同于树链剖分模板,我们还要新建一个辅助数组 low[i] ,表示节点 i 的重儿子的重儿子...(以此类推)可以到达的最深节点的编号,我们先对排序,之后查询的时候我们查询 之间的序列即可。
其实吧,上面只是代码实现的一部分,具体细节远不止这些,更多的细节请参考代码。
时间复杂度:极端情况约
代码:
(仅供参考)
T1:
#include<cstdio>
#include<cstring>
#include<map>
#include<algorithm>
using namespace std;
const int maxn=50005;
map<int,int>mp;
int k[maxn],p[maxn],x[maxn];
int n,m,P[maxn],f[maxn][505],T[maxn],sum[maxn],SUM;
void ready(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",k+i,p+i);
for(int i=1;i<=n;i++) scanf("%d",x+i);
for(int i=1;i<=n;i++) P[i]=max(P[i-1],p[i]);
for(int i=1;i<=n;i++){
mp[k[i]]+=p[i];//用map储存即可
P[i]=max(mp[x[i]],P[i]);
}
}
void dp(){
/*
f(i,j)表示已i为结尾且有j个时间段使用了魔法,可以得到的最大能量值
f(i,j)=MAX{f(k,j-1)}-P[i]+P[i+1] 1<j<=i-2
*/
int ans;
for(int i=1;i<=n;i++) SUM+=P[i];
ans=SUM;
if(m==0) {printf("%d\n",ans);return;}
for(int i=1;i<=n;i++)
f[i][1]=SUM-P[i]+P[i+1],
ans=max(ans,f[i][1]);
for(int j=2;j<=m;j++){
for(int k=1;k<=n;k++) T[k]=max(T[k-1],f[k][j-1]);//类似于预处理
for(int i=2*j-1;i<=n;i++)
f[i][j]=T[i-2]-P[i]+P[i+1],
ans=max(ans,f[i][j]);
}
printf("%d\n",ans);
}
int main(){
ready();
dp();
return 0;
}
T2:
#include<map>
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=20005;
map<pair<int,int>,double>mp,mp2;
map<pair<int,int>,double>::iterator it;
struct edge{
int to,next;
double R;
}e[maxn*5];
int head[maxn],np;
void adde(int u,int v,double R){
e[++np]=(edge){v,head[u],R};
head[u]=np;
e[++np]=(edge){u,head[v],R};
head[v]=np;
}
int n,m,from,to;
bool vis[maxn],done1[maxn],done2[maxn];
int d[maxn],fa[maxn],son[maxn];
double U,k=1.0,R_sum,R_fa[maxn],T[maxn];
void build(){
int R,u,v;
char S[5];
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%s%d",&u,&v,S,&R);
if(S[0]=='P') from=u,to=v,U=(double)R;//找到电源
else{
if(u>v) swap(u,v);
pair<int,int>pr=make_pair(u,v);
if(mp.count(pr)){//处理重边构成的并联
double R_=mp[pr];
mp[pr]=R_*R/(R+R_);//并联电路电阻的计算
mp2[pr]=max(mp2[pr],(double)R);
}
else mp[pr]=mp2[pr]=(double)R;
}
}
for(it=mp.begin();it!=mp.end();it++){
pair<int,int>pr=it->first;
k=min(k,mp[pr]/mp2[pr]);//k的计算
adde(pr.first,pr.second,it->second);
}
}
void bfs(int s){//寻找路径
memset(d,0x3f,sizeof(d));
memset(vis,0,sizeof(vis));
fa[s]=d[s]=0,vis[s]=true;
queue<int>q;
q.push(s);
while(!q.empty()){
int i=q.front();q.pop();
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(vis[j]) continue;
fa[j]=i,R_fa[j]=e[p].R;
vis[j]=true,d[j]=d[i]+1;
q.push(j);
}
}
}
int dfs(int i,double& sum){//寻找支路
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(done2[j]) continue;
sum+=e[p].R;
if(done1[j]) return j;//找到另一个交汇点
else {done2[j]=true;return dfs(j,sum);}
} return -1;
}
void solve(){
bfs(from);
for(int i=to;i;i=fa[i]) done1[i]=true,son[fa[i]]=i;//标记主线
for(int i=to;i!=from;i=fa[i]){
double R_max,R_=0;//R_:等效电阻
int cnt=0,pos;
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(done1[j]||done2[j]) continue;
T[++cnt]=mp[make_pair(min(i,j),max(j,i))];
done2[i]=done2[j]=true;//标记支线
pos=dfs(j,T[cnt]);
}
if(!cnt){R_sum+=R_fa[i];continue;}//当前路线不包含并联
for(int j=i;j!=pos;j=fa[j]) R_+=R_fa[j];
R_max=R_;
for(int j=1;j<=cnt;j++){
R_max=max(R_max,T[j]);
R_=R_*T[j]/(R_+T[j]);
}
k=min(k,R_/R_max);
R_sum+=R_,i=son[pos];
}
printf("%.2lf\n%.2lf\n",U/R_sum,U*k/R_sum);
printf("%d",1/0);
}
int main(){
build();
solve();
return 0;
}
T3:(暴力算法)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=100005;
int next[maxn*5],hash[maxn*5],first[maxn],last[maxn],np_;
void add_file(int u,int num){//链表实现按位置储存文件
hash[++np_]=num;
if(first[u]) next[last[u]]=np_;
else first[u]=np_;
last[u]=np_;
}
int hash_num(char* s){
int ret=0;
for(int i=0;s[i]!=0;i++) ret=ret*27+s[i]-96;
return ret;
}
struct edge{//储存树边
int to,next;
}e[maxn*2];
int head[maxn],np;
void adde(int u,int v){//添加树边
e[++np]=(edge){v,head[u]};
head[u]=np;
e[++np]=(edge){u,head[v]};
head[v]=np;
}
int n,m,k[maxn],dep[maxn],fa[maxn];
void dfs(int i,int Fa){
fa[i]=Fa,dep[i]=dep[Fa]+1;
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(j==Fa) continue;
dfs(j,i);
}
}
void build(){
int u,v;
char ext_nm[10];
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++) scanf("%d%d",&u,&v),adde(u,v);
for(int i=1;i<=n;i++){
scanf("%d",k+i);
for(int j=1;j<=k[i];j++){
scanf("%s",ext_nm);
add_file(i,hash_num(ext_nm));//插入链表
}
}
dfs(1,0);
}
int lca(int x,int y){
while(x!=y){
if(dep[x]<dep[y]) y=fa[y];
else x=fa[x];
}
return x;
}
int query_path(int x,int y,int id,bool del){//暴力处理文件夹
int ret=0;
while(x!=y){
if(dep[x]<dep[y]) swap(x,y);
for(int p=first[x];p;p=next[p]) if(hash[p]==id){
ret++;
if(del) hash[p]=-1;
}
x=fa[x];
}
for(int p=first[x];p;p=next[p])if(hash[p]==id){
ret++;
if(del) hash[p]=-1;
}
return ret;
}
void solve(){
char cmd[3][10];
int u,v,id;
while(m--){
scanf("%s%s",cmd[0],cmd[1]);
if(cmd[0][0]=='q'){
if(cmd[1][1]=='p'){
scanf("%d%d",&u,&v);
printf("%d\n",dep[u]+dep[v]-2*dep[lca(u,v)]);
} else {
scanf("%d%d%s",&u,&v,cmd[2]);
id=hash_num(cmd[2]+2);
printf("%d\n",query_path(u,v,id,false));
}
}
else{
scanf("%d%d%s",&u,&v,cmd[2]);
id=hash_num(cmd[2]+2);
printf("%d\n",query_path(u,v,id,true));
}
}
printf("%d",1/0);//Don't copy my code !
}
int main(){
build();
solve();
return 0;
}
T3:(正解)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=100005;
int next[maxn*5],hash[maxn*5],first[maxn],last[maxn],np_;
void add_file(int u,int num){//链表实现按位置储存文件
hash[++np_]=num;
if(first[u]) next[last[u]]=np_;
else first[u]=np_;
last[u]=np_;
}
int hash_num(char* s){
int ret=0;
for(int i=0;s[i]!=0;i++) ret=ret*27+s[i]-96;
return ret;
}
struct edge{//储存树边
int to,next;
}e[maxn*2];
int head[maxn],np;
void adde(int u,int v){//添加树边
e[++np]=(edge){v,head[u]};
head[u]=np;
e[++np]=(edge){u,head[v]};
head[v]=np;
}
struct ext_name{
int v,id;
const bool operator < (const ext_name& B)const {
return v<B.v;
}
}D[maxn*6];
bool cmp(const ext_name& A,const ext_name& B){
return A.v<B.v||(A.v==B.v&&A.id<B.id);
}
int n,m,k[maxn];
/*--------------------线段树----------------------*/
int rt,nowpos,lc[maxn*12],rc[maxn*12],sum[maxn*12],setv[maxn*12];
#define pushup(now) sum[now]=sum[lc[now]]+sum[rc[now]]
void pushdown(int now,int l,int r,int M){
if(setv[now]!=-1) return;
sum[lc[now]]=sum[rc[now]]=0;
setv[lc[now]]=setv[rc[now]]=-1;
setv[now]=0;
}
void build(int& now,int l,int r){
now=++nowpos;
if(l==r) {sum[now]=1;return;}
int M=(l+r)>>1;
build(lc[now],l,M);
build(rc[now],M+1,r);
pushup(now);
}
void update(int now,int l,int r,int x,int y){
if(x<=l&&r<=y) {setv[now]=-1,sum[now]=0;return;}
int M=(l+r)>>1;
pushdown(now,l,r,M);
if(y<=M) update(lc[now],l,M,x,y);
else if(x>M) update(rc[now],M+1,r,x,y);
else update(lc[now],l,M,x,y),update(rc[now],M+1,r,x,y);
pushup(now);
}
int query(int now,int l,int r,int x,int y){
if(x<=l&&r<=y) return sum[now];
int M=(l+r)>>1;
pushdown(now,l,r,M);
if(y<=M) return query(lc[now],l,M,x,y);
else if(x>M) return query(rc[now],M+1,r,x,y);
else return query(lc[now],l,M,x,y)+query(rc[now],M+1,r,x,y);
}
/*-------------------树链剖分--------------------*/
int fa[maxn],son[maxn],dep[maxn],sz[maxn];
int P,top[maxn],seg[maxn],rev[maxn],low[maxn];
int lP[maxn],tP[maxn],pos;
void dfs1(int i,int Fa){
dep[i]=dep[Fa]+1,fa[i]=Fa,sz[i]=1;
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(j==Fa) continue;
dfs1(j,i);
sz[i]+=sz[j];
if(sz[son[i]]<sz[j]) son[i]=j;
}
}
void load_files(int segl,int segr){
sort(D+lP[segl],D+tP[segr]+1,cmp);
}
void init_folder(int u){
int i=rev[u];
lP[u]=pos+1;
if(k[i]){
for(int p=first[i];p;p=next[p])
D[++pos].v=hash[p],D[pos].id=u;
tP[u]=pos;
}
else D[++pos].v=-1,D[pos].id=u,tP[u]=pos;//如果为空文件夹装载假文件
}
void dfs2(int i){
if(sz[i]==1)
load_files(seg[top[i]],seg[i]),low[i]=i;//将文件排序
if(son[i]){
top[son[i]]=top[i];
seg[son[i]]=++P,rev[P]=son[i];
init_folder(P);//将文件夹中的文件写入序列
dfs2(son[i]);
low[i]=low[son[i]];
}
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(top[j]) continue;
top[j]=j,seg[j]=++P,rev[P]=j;
init_folder(P);
dfs2(j);
}
}
int lca(int x,int y){
int fx=top[x],fy=top[y];
while(fx!=fy){
if(dep[fx]<dep[fy]) swap(fx,fy),swap(x,y);
x=fa[fx],fx=top[x];
}
if(dep[x]<dep[y]) return x;
return y;
}
int calc(int l,int r,int L__,int R__,int id,bool del){//最难懂的部分之一,用二分寻找子序列位置
ext_name Tmp;Tmp.v=id;
L__=lP[L__],R__=tP[R__];
int L_=lower_bound(D+L__,D+R__+1,Tmp)-D,L;
int R_=upper_bound(D+L__,D+R__+1,Tmp)-D,R;
if(L_>=R_) return 0;//没有找到
R_--;
int l_=R_+1,r_=L_-1;
L=L_,R=R_;
while(L<=R){//二分寻找
int M=(L+R)>>1;
if(D[M].id>=l) l_=M,R=M-1;
else L=M+1;
}
L=L_,R=R_;
while(L<=R){
int M=(L+R)>>1;
if(D[M].id<=r) r_=M,L=M+1;
else R=M-1;
}
if(l_>r_) return 0;
int TMP=query(rt,1,pos,l_,r_);//在线段树中查询
if(del) update(rt,1,pos,l_,r_);//删除文件
return TMP;
}
int query_path(int x,int y,int id,bool Access_y){//树链剖分中跳路径的方法
int fx=top[x],fy=top[y],ret=0;
while(fx!=fy){
ret+=calc(seg[fx],seg[x],seg[fx],seg[low[fx]],id,false);
x=fa[fx],fx=top[x];
}
if(Access_y) ret+=calc(seg[y],seg[x],seg[top[y]],seg[low[y]],id,false);
else ret+=calc(seg[y]+1,seg[x],seg[top[y]],seg[low[y]],id,false);
return ret;
}
int del_path(int x,int y,int id){
int fx=top[x],fy=top[y],ret=0;
while(fx!=fy){
ret+=calc(seg[fx],seg[x],seg[fx],seg[low[fx]],id,true);
x=fa[fx],fx=top[x];
}
ret+=calc(seg[y],seg[x],seg[top[y]],seg[low[y]],id,true);
return ret;
}
void build(){
int u,v;
char ext_nm[10];
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++) scanf("%d%d",&u,&v),adde(u,v);
for(int i=1;i<=n;i++){
scanf("%d",k+i);
for(int j=1;j<=k[i];j++){
scanf("%s",ext_nm);
add_file(i,hash_num(ext_nm));//插入到链表
}
}
dfs1(1,0);
seg[1]=top[1]=P=rev[1]=1;
init_folder(1);
dfs2(1);
build(rt,1,pos);//初始化线段树
}
void solve(){
char cmd[3][10];
int u,v,a,id;
while(m--){
scanf("%s%s",cmd[0],cmd[1]);
if(cmd[0][0]=='q'){
if(cmd[1][1]=='p'){
scanf("%d%d",&u,&v);
printf("%d\n",dep[u]+dep[v]-2*dep[lca(u,v)]);
}
else{
scanf("%d%d%s",&u,&v,cmd[2]);
a=lca(u,v),id=hash_num(cmd[2]+2);
printf("%d\n",query_path(u,a,id,true)+query_path(v,a,id,false));
}
}
else{
scanf("%d%d%s",&u,&v,cmd[2]);
a=lca(u,v),id=hash_num(cmd[2]+2);
printf("%d\n",del_path(u,a,id)+del_path(v,a,id));
}
}
printf("%d",1/0);//Dont't copy my code !
}
int main(){
build();
solve();
return 0;
}