牛客第三场补题ABDHJ
牛客第三场补题ABDHJ:
B. Auspiciousness
题目:
Dog Card is a card game. In the game, there are a total of 2n2n2n cards in the deck, each card has a value, and the values of these 2n2n2n cards form a permutation of 1∼2n1\sim 2n1∼2n. There is a skill that works as follows:
- Draw a card from the top of the deck.
- If the deck is empty, then skip to step 333, otherwise you guess whether the card on the top of the deck has a higher value than your last drawn card and draw a card from the top of the deck. If your guess is correct, then repeat this step, otherwise skip to step 3.
- End this process.
Nana enjoys playing this game, although she may not be skilled at it. Therefore, her guessing strategy when using this skill is simple: if the value of the last drawn card is less than or equal to nnn, then she guesses that the next card's value is higher; otherwise, she guesses that the next card's value is lower. She wants to know, for all different decks of cards (Obviously, there are (2n)!(2n)!(2n)! cases), how many cards she can draw in total if she uses the skill only once in each case. Since this number can be very large, please provide the answer modulo a given value.
思路:
n的大小是300.
但是因为之前的杭电第二场的概率等等相关因素,导致首先想到了概率,然后之后一直在想组合数,当然不过因为数论不是我搞的,所以后半程我都在划水和打酱油。中途有和队友讨论过可能是DP,不过想了一会就否定了,因为首先,我想不到如何转移,其次,当时如果我有了一些状态,我也无法得到之后的方案数目。好吧,我是废物。
放正解:考虑\(dp[i][j][0/1]\)表示已经用了i个小数,j个大数,最后一段是小数/大数的方案数。这个方案数的意思是,放下了i个小数,j个大数,此时是正确的。但是是不知道下一张卡牌,也就是(i+j+1)张是什么样子的。
为什么要分段:
先解释一下分段的正确性:
分段之后就相当于最后所有的答案里面,只要是正确的,就都是存储的是:\(小、大、小、大、小\) 当然也可是第一个是大数字。 因为是dp,所以每一种情况都会被我们记录下来,也就是在分段的情况下,每一种都是会被我们处理到的,这样记录答案是不会有空余的。因此分段是正确的。(关于为什么要分段,暂且暂停)
状态转移:
首先枚举i,j。对于每一个i,j.有以下两种情况:
- 如果现在最后是\(f[i][j][0]\),最后一个是小数,那么上一段一定是大数,我放进去了一段小数。
枚举最后一段的数字个数为k,就可以转移了:
\(dp[i][j][0]+=dp[i-k][j][1]*C(n-i+k,k)*1;\)
\(dp[i-k][j][1]\) 表示的是上一种的方案数,我这一次放进去了k个小数,首先我要先把这k个小数选出来,也就是上面的\(C(n-i+k,k)\),这\(k\)个数放进去的方案:因为现在必须保证是正确的,所以最后只能从小往大放数字,因为一定会猜测下一个数字是大的。因此最后要乘以1.表示\(k\)个数的排列一共有1种情况,是符合题意的。 - \(dp[i][j][1]=dp[i][j-k][0]*C(n-j+k,k)*1;\)这一种和上面一样,不再赘述。
记录答案:
状态转移是一个难点,但记录答案如果不好好想想的话,以后遇到就压根不会认为是dp了。
对于一个\(dp[i][j][0]\) 最后是小数,且目前为正确,想让他下一张就直接失败。(意思是下一张要比现在的最后一张小的情况).如果可以成功推理出失败的情况,把所有的相应位置失败的情况加起来就可以组成答案了。
...反正我想不到,还是看了别人的才懂.
\(如果dp[i][j][0] 是通过dp[i-k][j][1]推过来的,在这种情况下,考虑失败的情况:\)
意思是现在已经放进去了i-k小数,j个大数,最后以大数结尾,接下来我又放了k个小数是对的,再一张就不对了。
那么也就是最后又放进去了\(k+1\)张小数,然后在最后一张和倒数第二张之间,不是从小到大的关系。
贡献的答案是:\(dp[i-k][j][1]*C(n-(i-k),k+1)*k*fac[2*n-i-j-1]*(i+j+1);\)
-
C就是从剩下的小数里面继续选择出来k+1个小数的所有情况
-
这k+1个小数的排列中有k种是会使得最终失败的。因此乘上一个k。
比如:\(1\ 2\ 3\ 4\ 5\)之中,正确的放置方法是:12345 如果最后一个失败,那么一定是最后一个小于倒数第二个。最后一个可以是1 :\(2\ 3\ 4\ 5\ 1\).
2:$1\ 3\ 4\ 5\ 2 $
3 4同理,但是最后一个不可能是5.
所以最后一段是k+1个数的时候有k种可能得情况。 -
fac是还剩下的数字的个数的阶乘。
-
\((i+j+1)\)是指一种当前满足要求的方案可以抽的卡牌数目。
为什么要分段:
再考虑一下为什么要分段的问题,我能想到的可以让自己稍微信服一点的原因就是。通过分段之后我是正确的,且在代码的最后记录答案的过程中和段有很大关系。因为要枚举最后一段有几个数字的所有情况。
坦白说,如果不分段,我不会。所以我觉得要分段。
总结:
对于一个n是300的,要好好考虑dp可不可以。因为300甚至可以开n的三次方。
在计数DP里面,有的时候通过状态推理出合理的方案也是一个复杂的过程。不雅每一次都指望一个dp 一些转移方程,最后就能得到直接的结果。这个题目就是在dp的基础上,又加了无数的组合数学才记录出来的答案。
扬爷是这样说的:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Modint {
static int P;
int val;
Modint(int _val = 0) :val(_val %P) { format(); }
Modint(ll _val) :val(_val %P) { format(); }
Modint &format() {
if (val < 0) val += P;
if (val >= P) val -= P;
return *this;
}
Modint inv()const { return qpow(*this, P - 2); }//求val的逆元
Modint &operator+=(const Modint &x) { val += x.val;return format(); }
Modint &operator-=(const Modint &x) { val -= x.val;return format(); }
Modint &operator*=(const Modint &x) { val = 1LL * val * x.val % P;return *this; }
Modint &operator/=(const Modint &x) { return *this *= x.inv(); }
friend Modint operator-(const Modint &x) { return { -x.val }; }
friend Modint operator+(Modint a, const Modint &b) { return a += b; }
friend Modint operator-(Modint a, const Modint &b) { return a -= b; }
friend Modint operator*(Modint a, const Modint &b) { return a *= b; }
friend Modint operator/(Modint a, const Modint &b) { return a /= b; }
friend Modint qpow(Modint a, ll k) {
Modint ans = 1;
while (k) {
if (k & 1) ans = ans * a;
k >>= 1;
a = a * a;
}
return ans;
}
friend istream &operator>>(istream &is, Modint &x) {
ll _x;
is >> _x;
x = { _x };
return is;
}
friend ostream &operator<<(ostream &os, const Modint &x) { return os << x.val; }
};
int P;
int n;
namespace CNM {
const int N = 3e2 + 7;
int c[N][N];
void init(int n) {
for (int i = 0;i <= n;i++)
for (int j = 0;j <= i;j++)
c[i][j] = 0 < j && j < i ? (c[i - 1][j - 1] + c[i - 1][j]) % P : 1;
}
int C(int n, int m) {
if (n == m && m == -1) return 1; //* 隔板法特判
if (n < m || m < 0) return 0;
return c[n][m];
}
}
Modint fac[605];
Modint dp[305][305][2];
int Modint::P=1e9+7;
void solve(){
memset(dp,0,sizeof(dp));
cin>>n>>P;
Modint::P=P;
CNM::init(n);
fac[0]=1;
for(int i=1;i<=2*n;i++) fac[i]=fac[i-1]*i;
dp[0][0][0]=1; dp[0][0][1]=1;
Modint ans=0;
for(int i=0;i<=n;i++){
for(int j=0;j<=n;j++){
for(int k=1;k<=n;k++){
if(k<=i) dp[i][j][0]+=dp[i-k][j][1]*CNM::C(n-(i-k),k);
if(k<=j) dp[i][j][1]+=dp[i][j-k][0]*CNM::C(n-(j-k),k);
if(i<n && k<=i)ans+=dp[i-k][j][1]*CNM::C(n-(i-k),k+1)*k*fac[2*n-i-j-1]*(i+j+1);
if(j<n && k<=j)ans+=dp[i][j-k][0]*CNM::C(n-(j-k),k+1)*k*fac[2*n-i-j-1]*(i+j+1);
}
}
}
ans+=(dp[n][n][0]+dp[n][n][1])*(2*n);
cout<<ans<<endl;
}
signed main()
{
int T;
cin>>T;
while(T--){
solve();
}
return 0;
}
H. Until the Blue Moon Rises
思路:
补充知识点:
哥德巴赫猜想:任一大于等于6的整数都可写成三个质数之和。
比如:
\(6->2,2,2\\7->2,2,3\\8->2,3,3\\9->3,3,3\\10->2,3,5\\11->3,3,5\\12->2,3,7\\12->3,3,7\)
可以发现,上面哥猜可以解本题。
对于n>=3的情况,前面n-3个数,都用2,最后留下的总和大于等于6即可。
对于n=1的情况,判断自己。
对于n=2的的情况:如果sum是偶数,就一定可以分为两个质数的和。
如果是奇数:分成两个质数,只有可能一个是2,另一个是质数。
因为奇数只能分为一个奇数加一个偶数,两个都需要是偶数,所以必须偶数是2.
代码略。
D. Ama no Jaku
题目:
01的n*n矩阵,每一次可以让一行或者一列取反。问最小的操作次数,使得最终每一个行组成的二进制数字中的最小值大于每一个列的组成的二进制数字的max。
思路:
上面的可以总结为:必须全部为0,或者全部为1.
手玩几组之后可以发现:只有每一行的01和第一行完全一样或者完全不一样的时候才可以最终矩阵所有数字都一样。
那么我们记录有几个和第一行一样的和不一样的,最后更新答案即可。
用了bitset 但是其实不用也是可以过的。
#include<bits/stdc++.h>
using namespace std;
const int N=2005;
char ch[N][N];
bitset<N>bit[N];
bitset<N>bit2[N];
void solve(){
int n;
cin>>n;
for(int i=1;i<=n;i++) bit[i].reset();
for(int i=1;i<=n;i++) bit[0][i]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
char ch; cin>>ch;
if(ch=='1') bit[i][j]=bit2[j][n-(i-1)]=1;
else bit[i][j]=bit2[j][n-(i-1)]=0;
}
}
int num=(bit[1]&bit[0]).count();
int t=0,nt=0;
for(int i=2;i<=n;i++){
int p=(bit[i]&bit[0]).count();
int now=(bit[i]&bit[1]).count();
if(now==0 && p==n-num) nt++;
else{
if(now==num && p==now) t++;
else {
cout<<"-1\n";
return ;
}
}
}
int ans=0x3f3f3f3f;
ans=min(ans,nt+num);
ans=min(ans,t+1+(n-num));
ans=min(ans,nt+(n-num));
ans=min(ans,t+1+num);
//为什么有4种,首先要把所有的变一样,其次是变的全部为0或者全部为1.
cout<<ans<<endl;
}
int main(){
cin.tie(0);
ios::sync_with_stdio(false);
solve();
return 0;
}
J.Fine Logic
思路:
如果一次就可以输出答案,那么就跑一次拓扑排序,按照顺序输出就可以。
否则直接输出2:1~n和 n~1即可。(一定是最优秀的)
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
vector<int>tr[N];
int du[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
tr[x].push_back(y);
du[y]++;
}
queue<int>q;
for(int i=1;i<=n;i++){
if(du[i]==0) q.push(i);
}
vector<int>ans;
while(!q.empty()){
int t=q.front(); q.pop();
ans.push_back(t);
for(auto v:tr[t]){
du[v]--;
if(du[v]==0) q.push(v);
}
}
if(ans.size()==n){
cout<<"1\n";
for(auto v:ans)
cout<<v<<" ";
cout<<"\n";
}
else{
cout<<"2\n";
for(int i=1;i<=n;i++)
cout<<i<<" \n"[i==n];
for(int i=n;i>=1;i--)
cout<<i<<" \n"[i==1];
}
return 0;
}
A. World Fragments I
思路:
如果第一个是0,且y!=0 就不可以。
否则输出两者的差值。
signed main()
{
string x,y; cin>>x>>y;
ll a,b;
a=b=0;
for(auto c:x){
a=2*a+(c-'0');
}
for(auto c:y)
b=2*b+(c-'0');
if(a==0 && a!=b){
cout<<"-1\n";
}
else cout<<abs(a-b)<<endl;
return 0;
}