考試背景:
放假18個小時后,回到學校,隨即迎來8月第一場考試,考試時間從下午一點五十到五點十分,但可能由於剛放了假,同學們身心疲倦(邏輯上似乎說不通。。),都來的有些晚,考試時間推遲了五分鍾。天氣很熱,機房里空調開到十七度,吹出陣陣白氣,山大附中的同學沒有來,機房里非常安靜,只有零零星星敲擊鍵盤的聲音(其中有我敲快讀的聲音,可能會被旁邊那位大佬打一頓。。)。
考試過程:
開始考試后,閱讀題目,驚奇的發現,第一題近乎原題,但心里有些發虛,因為那道原題是我用桶排序水過的,正解是線段樹二分建樹統計貢獻,然而我並沒有打,於是兩分鍾先碼了一個暴力,拿到了40分,去看下面的題,於是乎,我進入了深深的絕望。。發現除了暴力dfs,我一點思路都沒有,於是又回到第一題,硬着頭皮打我並不是特別熟悉的線段樹,果不其然,一顆線段樹,硬生生調了一個半小時。。不過,最后還是調對了,T1最終得分90,T了一個點,后來經多方考察,發現用一個精(du)妙(liu)的技巧——循環展開,可以迅速A掉,hzoi-cbx大佬更是用暴力,跑出我正解七分之一的時間。。碼完第一題,我渾身是汗(調不出來急得。。),脫下了校服,開始打下面的題,我旁邊那位大佬卻冷得縮成了一團。。並沒有理他,繼續我的騙分之路。T2的思路,那是一點也沒有。。打了個暴搜瞬間過樣例,於是手模了一個大點,跑了20分鍾。。於是乎,T2最終得分20,和期望得分一樣,這實際上是一道神DP,旁邊那位成為全場唯一一個AC的%%%%%%%%%。T3實際上也差不多,由於我的位運算掌握的不好,並沒有看出所給式子的實際含義,於是只能碼一個暴力模擬,加上一些卡常技巧,本以為能拿到40分,結果卻只有30分,尷尬至極。全都碼完后,考試還剩半個多小時,反反復復檢查了好幾遍,給T1優化了一下碼風,T2加了一個剪枝,T3。。沒啥變化。。考試結束,最終得分90+20+30=140分,rank 3/52,rank1是外校的國賽銅牌選手,跟我們一個年級,拿到了200分,深感慚愧,rank2當然是旁邊的大佬,180分,也是碾壓全場,在日后的學習中,要多加借鑒,考出水平,考出素質,考出狀態。
考試題解:
T1:
同 [Tjoi2016&Heoi2016]排序 一題,把數串換做字符串,最后一次查詢換做全部輸出,但這絲毫不影響桶排的O(n)復雜度對於sort O(nlogn) 本身的優越性,加上我對桶排的熟悉度(用它水了好多題。。),於是我決定使用桶排來完成題目的要求,但觀察數據范圍,發現桶排的復雜度並不足以支撐這個題目,所以我們采用線段樹優化桶排的方式,來使我們的期望復雜度盡可能低。復雜度O(mlogn*26),m為操作數,每次區間修改logn,修改26次。由於線段樹自身的大常數,我們需要用一些卡常技巧(循環展開)來優化我們26倍的常數。
那么整體思路是,線段樹動態維護每一個區間內,a-z各自出現的個數,利用區間覆蓋,來修改區間排序后的值,利用懶標記下傳,來維護整個序列的整體性,與賦值的正確性,利用桶排思想進行區間覆蓋。
例如一串字母 ababcd ,其中 a出現2次,tr[x].a[a]=2,同理,tr[x].a[b]=2,tr[x].a[c]=1,tr[x].a[d]=1;那么我們按序輸出,也就是按序覆蓋,得到區間為aabbcd,完成了一次區間覆蓋操作,具體操作是要將操作區間所對應的線段樹序列上的節點信息合並為一個桶,遞歸統計答案。
其中懶標記維護的是一個區間內,出現的都是同一個字母的個數,所以是以懶標記為下標,維護一段區間長度即可。
最后統計答案時可以深搜遍歷一邊線段樹,將節點信息合並,完成答案統計。
代碼如下:

#include<bits/stdc++.h> #define re register using namespace std; int n,m,ans[30],tot=0; char s[200010]; struct node{int l,r,lz,a[30];}tr[800010]; inline int read(){ re int a=0,b=1; re char ch=getchar(); while(ch<'0'||ch>'9') b=(ch=='-')?-1:1,ch=getchar(); while(ch>='0'&&ch<='9') a=(a<<3)+(a<<1)+(ch^48),ch=getchar(); return a*b; } inline void pushdown(re int x){ if(tr[x].lz){ tr[x<<1].a[1]=tr[x<<1].a[2]=tr[x<<1].a[3]=tr[x<<1].a[4]=tr[x<<1].a[5]=tr[x<<1].a[6]=tr[x<<1].a[7]=tr[x<<1].a[8]=tr[x<<1].a[9]=tr[x<<1].a[10]= tr[x<<1].a[11]=tr[x<<1].a[12]=tr[x<<1].a[13]=tr[x<<1].a[14]=tr[x<<1].a[15]=tr[x<<1].a[16]=tr[x<<1].a[17]=tr[x<<1].a[18]=tr[x<<1].a[19]= tr[x<<1].a[20]=tr[x<<1].a[21]=tr[x<<1].a[22]=tr[x<<1].a[23]=tr[x<<1].a[24]=tr[x<<1].a[25]=tr[x<<1].a[26]=tr[x<<1|1].a[1]=tr[x<<1|1].a[2]= tr[x<<1|1].a[3]=tr[x<<1|1].a[4]=tr[x<<1|1].a[5]=tr[x<<1|1].a[6]=tr[x<<1|1].a[7]=tr[x<<1|1].a[8]=tr[x<<1|1].a[9]=tr[x<<1|1].a[10]= tr[x<<1|1].a[11]=tr[x<<1|1].a[12]=tr[x<<1|1].a[13]=tr[x<<1|1].a[14]=tr[x<<1|1].a[15]=tr[x<<1|1].a[16]=tr[x<<1|1].a[17]=tr[x<<1|1].a[18]=tr[x<<1|1].a[19]= tr[x<<1|1].a[20]=tr[x<<1|1].a[21]=tr[x<<1|1].a[22]=tr[x<<1|1].a[23]=tr[x<<1|1].a[24]=tr[x<<1|1].a[25]=tr[x<<1|1].a[26]=0; re int lazy=tr[x].lz; tr[x].lz=0; tr[x<<1].a[lazy]=tr[x<<1].r-tr[x<<1].l+1; tr[x<<1|1].a[lazy]=tr[x<<1|1].r-tr[x<<1|1].l+1; tr[x<<1].lz=tr[x<<1|1].lz=lazy; } } inline void pushup(re int x){ for(re int i=1;i<=26;i++) tr[x].a[i]=tr[x<<1].a[i]+tr[x<<1|1].a[i]; } inline void build(re int x,re int l,re int r){ tr[x].l=l;tr[x].r=r;tr[x].lz=0; if(l==r){tr[x].a[s[l]-'a'+1]=1;return;} re int mid=(l+r)>>1; build(x<<1,l,mid),build(x<<1|1,mid+1,r); pushup(x); } inline void query(re int x,re int l,re int r){ if(tr[x].l>=l&&tr[x].r<=r){ for(re int i=1;i<=26;i++) ans[i]+=tr[x].a[i]; return ; } pushdown(x); re int mid=(tr[x].l+tr[x].r)>>1; if(l<=mid) query(x<<1,l,r); if(r>mid) query(x<<1|1,l,r); } inline void change(re int x,re int l,re int r,re int k){ if(tr[x].l>=l&&tr[x].r<=r){ tr[x].a[1]=tr[x].a[2]=tr[x].a[3]=tr[x].a[4]=tr[x].a[5]=tr[x].a[6]=tr[x].a[7]=tr[x].a[8]=tr[x].a[9]=tr[x].a[10]= tr[x].a[11]=tr[x].a[12]=tr[x].a[13]=tr[x].a[14]=tr[x].a[15]=tr[x].a[16]=tr[x].a[17]=tr[x].a[18]=tr[x].a[19]= tr[x].a[20]=tr[x].a[21]=tr[x].a[22]=tr[x].a[23]=tr[x].a[24]=tr[x].a[25]=tr[x].a[26]=0; tr[x].a[k]=tr[x].r-tr[x].l+1; tr[x].lz=k; return ; } pushdown(x); re int mid=(tr[x].l+tr[x].r)>>1; if(l<=mid) change(x<<1,l,r,k); if(r>mid) change(x<<1|1,l,r,k); pushup(x); } inline void search(re int x) { if(tr[x].l==tr[x].r) for(re int i=1;i<=26;i++) if(tr[x].a[i]){s[++tot]=i+'a'-1;return;} pushdown(x); search(x<<1); search(x<<1|1); } signed main(){ n=read(),m=read(); scanf("%s",s+1); build(1,1,n); while(m--){ re int l=read(),r=read(),x=read(); ans[1]=ans[2]=ans[3]=ans[4]=ans[5]=ans[6]=ans[7]=ans[8]=ans[9]=ans[10]= ans[11]=ans[12]=ans[13]=ans[14]=ans[15]=ans[16]=ans[17]=ans[18]=ans[19]= ans[20]=ans[21]=ans[22]=ans[23]=ans[24]=ans[25]=ans[26]=0; query(1,l,r); if(x==1){ for(re int i=1;i<=26;i++) if(ans[i]){ change(1,l,l+ans[i]-1,i); l+=ans[i]; } continue; } if(x==0){ for(re int i=26;i;i--) if(ans[i]){ change(1,l,l+ans[i]-1,i); l+=ans[i]; } continue; } } search(1); printf("%s",s+1); return 0; }
T2:
可以利用奇襲一題的思路思考,將一個矩陣,拍做一列,發現一定恰好有n個點,遍布抽象序列,於是可以使用dp橫向轉移,真的是神思路。
設l[i]為到i列時結束了的左區間的個數,r[i]到i列開始的右區間個數
思路一:
1.有j個1放在右區間,那么也就有i-j放在了左區間。又因為在這一列之前結束的左區間在之前已經放上,所以就剩了i-j-l[i-1]個1,可以放在在第i列結束的左區間里,當然這些個1可以放在之前某一列里,那么對答案的貢獻就要乘上A(i-j-l[i-1],l[i]-l[i-1])了。
2.到i列時不放新的1,f[i][j]=f[i-1][j];如果放了一個:f[i][j+1]+=f[i-1][j]*(r[i]-j);解釋一下,新放的那個1,能放在i列已開始的右區間里,但要去掉已經放了的j個區間。
思路二:
我們發現題目除了對哪一行的一個區間內的1的個數有要求外,主要的大前提是每列不能出現多於1個的1,所以我們發現,其實主要是對列有要求,所以dp思路是對列進行。我們枚舉每一列,對右邊排了幾個1進行一一地枚舉,然后算出方案數,再對左邊枚舉,再算出方案數,(記得取%)
如此轉移即可,但注意,1轉移一定要在2轉移完成后再進行,保證不會有區間重復運算。
以下為思路二的實現:

#include<bits/stdc++.h> #define re register #define mod 998244353 #define int long long using namespace std; int n,m,sl[3001],sr[3001],f[3001][3001]; signed main(){ scanf("%lld%lld",&n,&m);f[0][0]=1; for(re int i=1,l,r;i<=n;i++) scanf("%lld%lld",&l,&r),++sl[l],++sr[r]; for(re int i=2;i<=m;i++) sl[i]+=sl[i-1],sr[i]+=sr[i-1]; for(re int i=1;i<=m;i++){ f[i][0]=f[i-1][0]; for(re int j=1;j<=i;j++) (f[i][j]=f[i-1][j-1]*(sr[i]-j+1)%mod+f[i-1][j])%=mod; for(re int j=sl[i-1];j<sl[i];j++) for(re int k=0;k<i;k++) (f[i][k]*=(i-k-j))%=mod; } printf("%lld\n",f[m][n]%mod); return 0; }
T3:
此題運用了01trie樹的思路,01字典樹主要用於解決求異或最值的問題。
01字典樹和普通的字典樹原理類似,只不過把插入字符改成了插入二進制串的每一位(0或1)。
1 int tol; //節點個數 2 LL val[32*MAXN]; //點的值 3 int ch[32*MAXN][2]; //邊的值 4 void init() 5 { //初始化 6 tol=1; 7 ch[0][0]=ch[0][1]=0; 8 } 9 void insert(LL x) 10 { //往 01字典樹中插入 x 11 int u=0; 12 for(int i=32;i>=0;i--) 13 { 14 int v=(x>>i)&1; 15 if(!ch[u][v]) 16 { //如果節點未被訪問過 17 ch[tol][0]=ch[tol][1]=0; //將當前節點的邊值初始化 18 val[tol]=0; //節點值為0,表示到此不是一個數 19 ch[u][v]=tol++; //邊指向的節點編號 20 } 21 u=ch[u][v]; //下一節點 22 } 23 val[u]=x; //節點值為 x,即到此是一個數 24 } 25 LL query(LL x) 26 { //查詢所有數中和 x異或結果最大的數 27 int u=0; 28 for(int i=32;i>=0;i--) 29 { 30 int v=(x>>i)&1; 31 //利用貪心策略,優先尋找和當前位不同的數 32 if(ch[u][v^1]) u=ch[u][v^1]; 33 else u=ch[u][v]; 34 } 35 return val[u]; //返回結果 36 }
1. 01字典樹是一棵最多 32層的二叉樹,其每個節點的兩條邊分別表示二進制的某一位的值為 0 還是為 1. 將某個路徑上邊的值連起來就得到一個二進制串。
2.節點個數為 1 的層(最高層)節點的邊對應着二進制串的最高位。
3.以上代碼中,ch[i] 表示一個節點,ch[i][0] 和 ch[i][1] 表示節點的兩條邊指向的節點,val[i] 表示節點的值。
4.每個節點主要有 4個屬性:節點值、節點編號、兩條邊指向的下一節點的編號。
5.節點值 val為 0時表示到當前節點為止不能形成一個數,否則 val[i]=數值。
6.可通過貪心的策略來尋找與 x異或結果最大的數,即優先找和 x二進制的未處理的最高位值不同的邊對應的點,這樣保證結果最大。
對於此題我們發現,最多有2^30種不同的初始值。但是,對於確定的m+1種結果,我們可以確定每一位異或0還是1使得結果最大,而且對手會根據我選的更改答案。觀察式子對手做的是將 x 在二進制下左移一位。假設在異或 i 個數后左移,等價於開始時先左移,然后把前 i 個數左移一位。問題轉化為選一個數,使它左移一位后,與 m+1 個給定數分別異或的最小值最大。將 m+1 個數建立一棵字典樹,從上到下遍歷來最大化結果:走到一個點時,如果往下只有 0,說明我們這一位取 1異或后只能是 1,累計結果中加上這一位的值,只有 1 也一樣;如果既有 0 又有 1,說明這一位無論怎么取最后都是 0,分別往下走即可。時間復雜度 O(nm)。

#include<bits/stdc++.h> #define re register #define ll long long using namespace std; ll n,m,lim,sum[1000010],a[1000010],cnt,dat[1000010]; ll t[1000010],ch[3000010][2],id ,tot=0,ans[1000010]; inline int read(){ re int a=0,b=1; re char ch=getchar(); while(ch<'0'||ch>'9') b=(ch=='-')?-1:1,ch=getchar(); while(ch>='0'&&ch<='9') a=(a<<3)+(a<<1)+(ch^48),ch=getchar(); return a*b; } inline ll calc(re ll x){ return ((x<<1)/lim+(x<<1))%lim; } inline void insert(re ll x){ for(re int i=0;i<n;i++) t[i]=((x>>(n-i-1))&1); re int u=0; for(re int i=0;i<n;i++){ if(!ch[u][t[i]]) ch[u][t[i]]=++id; u=ch[u][t[i]]; } } inline void dfs(re ll x,re ll now,re ll k){ if(k<0){ans[++cnt]=now;return;} if(ch[x][0]&&!ch[x][1]) dfs(ch[x][0],now^(1<<k),k-1); if(ch[x][1]&&!ch[x][0]) dfs(ch[x][1],now^(1<<k),k-1); if(ch[x][0]&& ch[x][1]) dfs(ch[x][0],now,k-1),dfs(ch[x][1],now,k-1); } signed main(){ n=read(),m=read();lim=(1<<n); for(re ll i=1;i<=m;i++) a[i]=read(); for(re ll i=m;i>=1;i--) sum[i]=sum[i+1]^a[i]; for(re ll i=0;i<=m;i++){ cnt^=a[i]; dat[i]=calc(cnt)^sum[i+1]; insert(dat[i]); } cnt=0; dfs(0,0,n-1); sort(ans,ans+cnt+1); for(re ll i=cnt;i>=1;i--) if(ans[i]==ans[cnt]) tot++; printf("%lld\n%lld\n", ans[cnt],tot); return 0; }
考試反思:
這場考試中,雖然拿到了rank3,但還有很多不足之處,T2,T3兩題都是好題,卻一點思路也沒有,我還有很大的進步空間,還有很多知識需要去鞏固加強,還有很多能力需要去培養加固。模板一定要熟悉,思路一定要清晰,一定要有耐心去碼,去調試,不到考試結束,絕不放棄。在時間安排上,也要盡可能趨於合理,學習考試技巧,向他人學習。