6.11考試總結(NOIP模擬)7
背景
時間分配與得分成反比,T1 20min 73pts,T2 1h 30pts,T3 2h 15pts(沒有更新tot值,本來應該是40pts的,算是本次考試中最遺憾的地方了吧),改起來就是T3比較難改,其他的還好。。。
兩位隊爺沒考,戰神也出了點意外,讓我們這些菜雞鑽了空子。
T1 匹配
前言
我就沒想到模擬賽會出這種水題,正解的話hash與KMP都可以,只可惜我只留下20分鐘給這題,實力有限,時間有限,就草草打了個暴力。出乎意料整到了\(73pts\)屬實出乎意料。。
解題思路
就講一下Hash的做法吧,至於KMP做法請參考@fengwu的blog。
對於A的字首可以用常規方法直接依次算,使冪次方數隨著下標的遞增而遞減,
但是為了迎合後面對於B字尾的計算,我們這裡令冪次方隨下標遞增而遞增,先初始化一下,整出base值的若干次方存入p陣列,因此求出A的Hash:
然後對於B的字尾就可以直接從後往前正常掃就好了。
\[ha[i]=ha[i+1]\times base+s[i] \]之後我們就可以直接A的字首和B的字尾兩個串直接判等了,陣列要開的大一點。
code
#include<bits/stdc++.h> #define int unsigned long long using namespace std; const int N=2e5+10,base=13331; int T,n,m,ans,p[N],h1[N],h2[N]; char ch,s[N],s2[N]; bool vis[30]; #undef int int main() { #define int unsigned long long scanf("%llu",&T); p[0]=1; for(int i=1;i<N;i++) p[i]=p[i-1]*base; while(T--) { memset(vis,false,sizeof(vis)); ans=0; scanf("%llu%llu",&n,&m); scanf("%s",s+1); getchar(); scanf("%c",&ch); for(int i=1;i<=n;i++) vis[s[i]-'a']=true; if(!vis[ch-'a']) { cout<<0<<endl; continue; } for(int i=1;i<=m;i++) s2[i]=s[i]; s2[++m]=ch; for(int i=1;i<=m;i++) { h1[i]=h1[i-1]+s[i]*p[i-1]; // cout<<h1[i]<<" "<<h1[i-1]<<" "<<s[i]<<" " <<p[i-1]<<endl; } // cout<<endl; h2[m+1]=0; for(int i=m;i>=1;i--) h2[i]=h2[i+1]*base+s2[i]; /* for(int i=1;i<=m;i++) cout<<h2[i]<<' ';*/ for(int len=m;len>=1;len--) { int ha1=h1[len],ha2=h2[m-len+1]; if(ha1==ha2) { ans=len; break; } } printf("%llu\n",ans); } return 0; }
T2 回家
前言
考場上這個題第一眼就看出了是Tarjan割點,然後想了想Tarjan超出了我的能力範圍,就老老實實打暴力騙分去了。。。
解題思路
解題思路來自@fengwu,有一種雙端掃的感覺。。。
dfn[i]儲存i的時間戳,low[i]表示i所在聯通塊的最早dfs到的點的時間戳(話說這東西Tarjan不是講過嗎)vis用於從n節點向上進行更新。(vis[n]要初始化為true)
然後從1節點開始進行dfs,所連通的節點的狀態有兩種情況:
-
該節點未被掃過: 先對於該節點進行dfs,然後更新現在節點的low值,如果vis[to]是true也就是聯通節點可以走到n,向上更新現在節點的vis為true。如果現在節點的時間戳小於等於聯通節點所在聯通塊的最小時間戳並且聯通節點可以到達n節點,那麼這個點就是一個必經點。
-
該節點被掃過:用聯通節點的時間戳來更新現在節點的low,為什麼不用聯通節點的low呢,以下圖為例:
假設現在節點是6號節點,要掃3號節點了,3號節點的時間戳是7,low是1如果我們用low來更新,那無異與除7之外ia所有節點都是一個聯通塊的,這顯然是不可以的。
最後輸出就好了,注意必經點要排一下序。
code
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int T,n,m,cnt,tim,dian[N],dfn[N],low[N];
int tot,ver[N<<1],head[N],nxt[N<<1];
bool vis[N],b[N];
inline void add_edge(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void init()
{
tot=cnt=tim=0;
memset(vis,false,sizeof(vis));
memset(head,0,sizeof(head));
memset(nxt,0,sizeof(nxt));
memset(ver,0,sizeof(ver));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
}
void dfs(int x)
{
dfn[x]=low[x]=++tim;
for(int i=head[x];i;i=nxt[i])
{
int to=ver[i];
if(!dfn[to])
{
dfs(to);
low[x]=min(low[to],low[x]);
if(vis[to])
vis[x]=true;
if(low[to]>=dfn[x])
if(x!=1&&vis[to])
dian[++cnt]=x;
}
else
low[x]=min(low[x],dfn[to]);
}
}
int main()
{
scanf("%d",&T);
while(T--)
{
init();
scanf("%d%d",&n,&m);
for(int i=m,x,y;i>=1;i--)
{
scanf("%d%d",&x,&y);
if(x==y)
{
m--;
continue;
}
add_edge(x,y);
add_edge(y,x);
}
vis[n]=true;
dfs(1);
sort(dian+1,dian+cnt+1);
printf("%d\n",cnt);
for(int i=1;i<=cnt;i++)
printf("%d ",dian[i]);
printf("\n");
}
return 0;
}
T3 壽司
前言
挺可惜的,考場上2h想到了40分的打法,也打出來了,就是tot沒有清零喜提15pts。
解題思路
暴力
暴力的話40pts比較好想,先化環為鏈,再對於每一個長度為n的區間,先求一下每個R節點之前以及之後的R分別到兩端的距離和,用字首字尾和維護,然後對於移到左邊的個數進行列舉,更新。設到左側的個數為cnt,因為不是每一個點都要移到端點,不難發現我們需要減去一部分:
\[\sum\limits_{i=1}^{cnt-1} i=\dfrac{cnt\times(cnt-1)}{2} \]對於右端點的處理也是如此,優化的話就是二分一下對於區間的右半部分向右移,左半部分向左移,不難發現中間節點的座標與區間是單調的,我們可以暴力處理第一個,對於後面的挨個搜就行了,還需要一個全域性字首和複雜度為\(O(n)\)但是我們打出來。。。可以參考 @zxb的程式碼,下面給出暴力的程式碼
\[一定要清零tot值 \]code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
int T,n,ans,tot,temp,cnt,ch[N],q[N],h[N];
char s[N<<1];
void work(int x)
{
// memset(q,0,sizeof(q));
// memset(h,0,sizeof(h));
int r=x-1,lb=0;
for(int i=x;i<=2*n;i++)
{
r++;
if(s[i]=='B')
lb++;
if(lb==tot)
break;
}
if(lb!=tot)
return ;
for(int i=x;i<=r;i++)
if(s[i]=='R')
{
ch[++cnt]=i;
q[cnt]=q[cnt-1]+i-x;
}
h[cnt+1]=0;
for(int i=cnt;i>=0;i--)
h[i]=h[i+1]+r-ch[i];
for(int i=0;i<=cnt;i++)
{
int sum=0;
sum=q[i]-(i+1-1)*(i-1)/2+h[i+1]-(cnt-i+1-1)*(cnt-i-1)/2;
if(sum>=0)
ans=min(ans,sum);
}
}
#undef int
int main()
{
#define int register long long
#define ll long long
scanf("%lld",&T);
while(T--)
{
ans=INT_MAX;
tot=0;
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;i++)
tot+=(s[i]=='B');
if(n-tot>tot)
{
for(int i=1;i<=n;i++)
s[i]=(s[i]=='B')?'R':'B';
tot=n-tot;
}
for(int i=1;i<=n;i++)
s[i+n]=s[i];
for(int i=1;i<=n;i++)
{
if(s[i]=='R')
continue;
temp=cnt=0;
work(i);
}
printf("%ld\n",ans);
}
return 0;
}
正解
思路非常的巧妙來自zhanshen@zero4338,l[i]表示i到左端點的距離(也就是該點左邊B的個數)可以得出以下式子:
\[\sum\limits_{i=1}^{tot_R}\min(l[i],r[i]) \]\[=\sum\limits_{i=1}^{tot_R}\dfrac{l[i]+r[i]-|l[i]-r[i]|}{2} \]\[=\dfrac{tot_B\times tot_R}{2}+\sum\limits_{i=1}^{tot_R}\dfrac{|l[i]-r[i]|}{2} \]接下來我們只需要處理後半段就行了,對於後半段,我們先壓入小根堆裡,然後再對於B和R的情況分別進行處理:
- 掃到B字元:首先把B移到右邊端點後所有的l都減了1,r都加了1,因此加上負數數量*2,我們需要將之前處理P字元的進行更新,如果堆頂的值大於掃過的B數量*2,直接break,剩下的交給後面處理,對於相等的,計算移動的R字元本身,給sum減去2,處理完堆裡的之後再將正數的貢獻加上就好了。
- 掃到R字元:先不做處理,將它的貢獻壓入堆裡,然後更新正負數的值。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
int T,n,sum,maxn,tb,tr,z,f,tot,l[N],r[N];
char s[N];
priority_queue<int,vector<int>,greater<int> > q;
void init()
{
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
z=f=tb=tr=sum=tot=maxn=0;
while(!q.empty())
q.pop();
}
#undef int
int main()
{
#define int register long long
#define ll long long
scanf("%lld",&T);
while(T--)
{
init();
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;i++)
{
l[i]=l[i-1];
if(s[i]=='B')
{
tb++;
l[i]++;
}
else
tr++;
}
for(int i=1;i<=n;i++)
if(s[i]=='R')
{
r[i]=tb-l[i];
sum+=abs(l[i]-r[i]);
if(l[i]-r[i]>0)
{
q.push(l[i]-r[i]);
z++;
}
else f++;
}
maxn=max(maxn,sum);
for(int i=1;i<n;i++)
{
if(s[i]=='B')
{
sum+=2*f;
tot++;
while(!q.empty())
{
if(q.top()>2*tot)
break;
if(q.top()==2*tot)
sum-=2;
z--;
f++;
q.pop();
}
sum-=2*z;
maxn=max(maxn,sum);
}
else
{
z++;
f--;
q.push(tb+2*tot);
}
}
printf("%lld\n",(tr*tb-maxn)/2);
}
return 0;
}