Edu Round 170 Review
A
分析
一個很顯然的根據字首劃分的貪心,直接指標模擬就好了。
Code
#include<bits/stdc++.h>
using namespace std;
int main()
{
int t;
cin>>t;
while(t--)
{
string a,b;
cin>>a>>b;
int l1=a.length(),l2=b.length();
int p=0;
while(p<l1&&p<l2&&a[p]==b[p])p++;
if(p>0)p--;
cout<<l1+l2-p<<'\n';
}
}
B
分析
打表就發現需要輸出的就是 \(2^n\) ,然而這道題我卡了十多分鐘,實在是有點sb了。
具體證明的話可以用數學歸納法,也可以直接證明,都是比較容易的。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD=1e9+7;
inline int power(int a,int b)
{
int ans=1;
while(b)
{
if(b&1)ans=ans*a%MOD;
a=a*a%MOD;
b>>=1;
}
return ans%MOD;
}
const int N=2e5+1;
int t,n[N],k[N];
inline void solve()
{
cin>>t;
for(int i=1;i<=t;++i)scanf("%d",&n[i]);
for(int i=1;i<=t;++i)scanf("%d",&k[i]),printf("%lld\n",power(2,k[i]));
}
signed main()
{
solve();
}
C
分析
其實思路上面很簡單,只需要以每個數為開頭貪心地連續去選擇儘可能多的數字就可以了。
注意這樣做是 \(O(n^2)\) 的,那麼考慮如何在列舉的時候快速地從現在的答案推知下一個答案。
很容易想到一個簡化的莫隊,從 \([l,r]\) 到 \([l+1,r+1]\) ,實際上只是增加了一個數,去除了一個數而已,透過增量就可以計算出來。
邊界寫錯了吃了兩發罰時(實際上可以不需要去重的,所以說本人的找屎能力真的強)。
又成功地在模擬題上面寫出了一道屎山程式碼。。。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename T>inline void re(T &x)
{
x=0;int f=1;char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
x*=f;
}
template<typename T>inline void wr(T x)
{
if(x>9)wr(x/10);
putchar(x%10^48);
}
int t,n,k,a[200010];
pair<int,int>b[200010];
inline void solve()
{
re(n),re(k);
for(register int i=1;i<=n;++i)re(a[i]);
sort(a+1,a+n+1);a[n+1]=0;
int cnt=1,p=0;
for(register int i=1;i<=n;++i)
{
if(a[i]!=a[i+1])
{
b[++p].first=a[i];
b[p].second=cnt;
cnt=1;
}
else cnt++;
}
int ans=-1,lasl=0,lasr=0;
int lasans=0;
for(register int i=1;i<=p;++i)
{
if(!lasans)
{
int p1=i+1;lasans=b[i].second;
while(p1<=p&&p1-i+1<=k&&b[p1].first-b[p1-1].first==1)lasans+=b[p1++].second;
ans=max(ans,lasans),lasl=i,lasr=p1-1;
}
else
{
int nowr=lasr+i-lasl;
if(b[nowr].first-b[nowr-1].first!=1||nowr>p)
{
i=nowr-1;
lasans=lasl=lasr=0;
continue;
}
lasans-=b[i-1].second,lasans+=b[nowr].second;
ans=max(ans,lasans);
}
}
wr(ans),putchar('\n');
}
signed main()
{
re(t);
while(t--)
solve();
return 0;
}
D
分析
因為實現能力太差,C做了一個小時,導致D只有不到二十分鐘的時間了,大概掃過去想了一個很騷的做法,就是倒敘列舉然後貪心地選擇貢獻即可,結果發現題看錯了。。。
這種涉及到抉擇的題發現貪心是假的,那肯定就是DP了。
那其實階段和狀態也很容易設計出來。
定義 \(dp[i][j]\) 為前 \(i\) 個數字 選擇了 \(j\) 點智力加點能夠獲得的分數(注意此時體力的加點一定是唯一確定的,因此我們沒有必要單獨開一維來單獨記錄,前提是我們需要統計出來一個陣列來記錄任意某個位置 \(pos\) 之前一共出現了多少個 \(0\) ) 。
那麼轉移方程也非常的水到渠成:
如果 \(a_i=0\) ,那麼所有的 \(dp[i][j]=\max(dp[i-1][j],dp[i-1][j-1])\)
如果 \(a_i>0\) ,那麼所有的 \(dp[i][j](j\ge a_i)=dp[i-1][j]+1\)
如果 \(a_i<0\) ,那麼所有的 \(dp[i][j](j\le sum0-(-a_i))=dp[i-1][j]+1\)
注意到我們會多次執行區間加的操作,實際上你可以寫一個線段樹,或者是樹狀陣列,然而都太麻煩或者不好實現(對我來說)。
我們可以直接採用差分陣列,但是缺陷就在於,雖然可以多次進行區間加法,但是一隻能統計一次。觀察到這道題 \(m\) 的資料範圍非常小,所以每次遇到一個 \(0\) 之前,我們就暴力地統計一遍差分陣列,然後加到 \(dp\) 陣列裡面,而後進行 \(0\) 處該進行的轉移,再把差分陣列清零,重新進行後續的區間加操作即可。
Code
#include<bits/stdc++.h>
using namespace std;
template<typename T>inline void re(T &x)
{
x=0;int f=1;char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
x*=f;
}
template<typename T>inline void wr(T x)
{
if(x>9)wr(x/10);
putchar(x%10^48);
}
const int N=2e6+100;
int a[N],dp[5010];
int n,m;
int s[5010];
inline void add(int l,int r,int v)
{
s[l]+=v,s[r+1]-=v;
}
inline void calcsum()
{
for(int i=1;i<=m;++i)
s[i]+=s[i-1];
for(int i=0;i<=m;++i)
dp[i]+=s[i],s[i]=0;
}
inline void pre()
{
re(n),re(m);
for(register int i=1;i<=n;++i)re(a[i]);
}
inline void solve()
{
int sum0=0;
for(register int i=1;i<=n;++i)
{
if(a[i]==0)
{
calcsum();
for(register int j=++sum0;j>=1;--j)
dp[j]=max(dp[j],dp[j-1]);
}
else if(a[i]>0)
{
if(sum0<a[i])continue;
add(a[i],sum0,1);
}
else
{
if(sum0<abs(a[i]))continue;
add(0,sum0+a[i],1);
}
}
calcsum();
int ans=-1;
for(register int i=0;i<=m;++i)ans=max(ans,dp[i]);
wr(ans);
}
int main()
{
pre();
solve();
return 0;
}
/*
dp[i][j] 前i個數字 擁有 j 點智力可以獲得的分數
all dp[i][j]=dp[i-1][j];
if(0)
dp[i][j]=max dp[i][j],dp[i-1][j-1];
if(+)
dp[i][j]=add(a[i],sum0[i],1);
if(-)
dp[i][j]=add(0,sum0[i]+a[i],1);
寫的時候的問題:
字首和之後沒有給dp 0加上去
統計差分 和 進行 O(m) 轉移的順序搞反了
*/
E
非常好的一道計數題,讓我“溫習“了一下在高二的時候學習過的卡特蘭數。
前置知識
卡特蘭數
定義背景
由於這一類組合數對於實際問題有意義,所以才會被單獨討論,它適用於如下情況:
- 有 \(n\) 個人排成一行進入劇場。入場費 5 元。其中只有個 \(n\) 人有一張 5 元鈔票,另外人 \(n\) 只有 10 元鈔票,劇院無其它鈔票,問有多少種方法使得只要有 10 元的人買票,售票處就有 5 元的鈔票找零?
- 有一個大小為 \(n\times n\) 的方格圖開始每次都只能向右或者向上走一單位,不走到對角線上方(但可以觸碰)的情況下到達右上角有多少可能的路徑?
- 在圓上選擇 \(2n\) 個點,將這些點成對連線起來使得所得到的條 \(n\) 線段不相交的方法數?
- 對角線不相交的情況下,將一個凸多邊形區域分成三角形區域的方法數?
- 一個棧(無窮大)的進棧序列為 \(1-n\) 有多少個不同的出棧序列?
- \(n\) 個結點可構造多少個不同的二叉樹?
- 由 \(n\) 個\(+-1\) 組成的序列,滿足任意字首和非負,有多少種構造方案?
公式
-
\(H_n=\frac{C_{2n}^{n}}{n+1}\)
-
\(H_n=C_{2n}^n-C_{2n}{n-1}\)
-
\(H_n=H_{n-1}\times \frac{4n-2}{n+1}\)
-
DP的方式求(卡特蘭數實際上是一個匹配的過程)
定義 \(f_{i,j}\) 為,前 \(i\) 個數,剩下了 \(j\) 個沒有匹配的方案數,那麼最後的答案就是 \(f_{2n,0}\)
轉移方程也很好寫,
$$f_{i,j}+=f_{i-1,j-1}$$ 當前作為左括號。
$$f_{i,j}+=f_{i-1,j+1}$$ 當前作為右括號並和一個左括號匹配。
分析
我們欽定兩個人分別為 \(A,B\) ,明顯的是這道題中 \(1\) 是一個比較特殊的花色,對於其他普通的花色,我可以兩個人選的一樣,然後兩個人內部進行分配使得 \(A\) 總是贏;也可以暫時讓 \(B\) 的牌多於 \(A\) ,後面拿若干張的 \(1\) 來進行彌補性配對。
我們考慮如何進行這個計數DP。
對於 \(1\) 花色特殊考慮,我們先不處理它。
對於其他任意一種顏色,我們應用第一種分配策略時,把 \(m\) 張牌均分給兩個人,首先假定這個 \(1-m\) 的序列已經降序排列好了。
那麼我們從大到小以此給兩人分配,發現如果任何時刻,我給 \(B\) 分配的牌都必須要小於等於給 \(A\) 分配的牌,才能夠滿足題設條件。
那麼這實際上就是一個卡特蘭數 \(H_{\frac{m}{2}}\),可以比較性地發現。
我們應用第二種分配時,很容易想到先選出 \(k\) (偶數)個來不進行配對之後和 \(1\) 配,然後剩下的 \(m-k\) 個按照卡特蘭數配對。
看起來沒什麼問題,實際上也確實沒問題,但在這道題的計數規則下就是錯誤的,由於我選出來不配對的幾個數實際上是歸於 \(A\) 的,這就意味著分別屬於 \(A\) 的參與匹配的集合 和 A的暫時不參與匹配集合 的兩個數,可能會被重複計算。
反觀我們用 DP 求卡特蘭數的過程,實際上就已經把這裡我們需要的答案給計算出來了,我們想要剔除 \(k\) 個,然後剩下的配對,那麼方案數就是上面提到的 \(f_{i,k}\) ,那麼我們轉移起來也就相對來說很好想了。
\(dp_{i,j}\) 的定義是 前 \(i\) 個花色 (不包括 \(1\) ),一共剩下了 \(j\) 個的方案數。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD=998244353,N=600;
int dp[N][N],n,m;
inline int power(int a,int b)
{
int ans=1;
while(b)
{
if(b&1)ans=ans*a%MOD;
a=a*a%MOD;
b>>=1;
}
return ans%MOD;
}
inline void garbage_bin()
{
int C[N][N],catalan[N];
for(register int i=0;i<=m;++i)
{
C[i][0]=1;
for(register int j=1;j<=i;++j)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
}
catalan[0]=1;
for(register int i=1;i<=m;i++)
catalan[i]=catalan[i-1]*(4*i-2)%MOD*power(i+1,MOD-2)%MOD;
}
int cat[N][N];
void pre()
{
cin>>n>>m;
cat[0][0]=1;
for(register int i=1;i<=m;++i)
for(register int j=0;j<=m;++j)
{
if(j-1>=0)cat[i][j]+=cat[i-1][j-1],cat[i][j]%=MOD;
cat[i][j]+=cat[i-1][j+1],cat[i][j]%=MOD;
}
}
void solve()
{
dp[0][0]=1;
for(register int i=1;i<=n-1;++i)
{
for(register int j=0;j<=m;j+=2)
for(register int k=0;k<=j;k+=2)
dp[i][j]+=dp[i-1][j-k]*cat[m][k]%MOD,dp[i][j]%=MOD;
}
int ans=0;
for(register int j=0;j<=m;j+=2)
ans+=cat[m][j]*dp[n-1][j]%MOD,ans%=MOD;
cout<<ans;
}
// 4 3 2 1
// 12A + 4A3B
// 13A + 4A2B
// 14A + 3A2B
// 23A + 4A1B
// 24A + 3A1B
// 34A + 2A1B
//dp 4 2=dp 3 1 (2)+dp3 3 (1)
signed main()
{
pre();
solve();
return 0;
}
好久沒有寫過題解了。。。。