Logo Fee_cle6418的博客

博客

InfOJ Round 2 题解

2023-04-03 21:55:09 By Fee_cle6418

本次比赛中,ABD 的代码长度均不超过 800 byte,是当之无愧的小清新比赛!

A

先考虑 $B=2$。首先,二进制表示全部是 1 的数肯定全都是满足要求的。

还有什么数可能满足要求?注意到,如果二进制表示里只有一位不是 1,也是满足要求的。

而容易发现只有这两种数满足要求,直接计算即可。

推广,当 $B$ 任意时,容易发现 x(B-1)(B-1)(B-1)(B-1)(B-1) 满足要求,并且把其中任意一个 $B-1$ 换成 $B-2$ 也满足要求。

怎么计算满足要求的数 $x$ 的个数呢?

  • 如果 $x$ 的位数小于 $n$ 的,都满足要求,容易直接 $O(1)$ 计算。
  • 如果 $x$ 的位数等于 $n$ 的,但是首位小于 $n$,也满足要求,容易直接 $O(1)$ 计算。
  • 如果 $x$ 的位数等于 $n$,首位等于 $n$,找到 $n$ 除了首位之外最长的一段 $B-1$,如果 $B-2$ 在这之前都可以;还要特判一下恰好在分界这里可不可以。

时间复杂度 $O(\log n)$。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef vector<int> ve;
const int mod=998244353;
ll n,B,a[70];
void Solve(){
    scanf("%lld%lld",&n,&B);
    ll tn=n,ans=0;
    int s=0;
    while(tn)a[++s]=tn%B,tn/=B;
    for(int i=1;i<s;i++)ans+=(B-1)*i;
    ans+=(a[s]-1)*s;
    int lst=0;
    for(int i=s-1;i>=1;i--){
        if(a[i]<B-1){
            lst=i;
            break;
        }
    }
    if(lst==0)ans+=s;
    else {
        ans+=s-1-lst;
        if(a[lst]==B-2){
            bool ok=1;
            for(int i=lst-1;i>=1;i--)if(a[i]<B-1)ok=0;
            if(ok)ans++;
        }
    }
    cout<<ans<<'\n';
}
int main(){
    int t;
    scanf("%d",&t);
    while(t--)Solve(); 
}

B

这题曾经想放进我和 gyh 出的月赛。但是好像因为结论过于简单而且可以用各种方法得出换掉了。

结论是:设 1 的位置为 $a_1,\dots,a_m$,若 $(n-a_i)$ 的异或和为 0 则后手赢否则先手赢。

首先给序列做前缀和(把原序列看成差分,那么在新序列上的操作就是区间异或),游戏变为:

  • A,B 轮流作如下操作:选择一个位置 $i\ (i

我们断言,这个游戏的结果等价于以下游戏:

  • A,B 轮流作如下操作:选择一个位置 $i\ (i减一、$a_{i+1}\sim a_j$ 都加上一(这时序列不一定总是 01 序列了)。不能操作的人输。[游戏 2]

证明:注意到在游戏二中每一单位 $a_i$ 是独立的(把 $a_i$ 看成 $n$ 堆石子分别的石子数,则一个石子的操作不影响另一石子),所以游戏可以看成 $\sum a_i$ 个游戏的和。所以游戏的 SG 是每个石子的单位游戏的 xor。所以把游戏二中的 $a_i$ 在 mod 2 意义下操作不影响结果。所以游戏二等价于游戏一。

那么只需要解决游戏二。可以归纳证明在第 $i$ 堆石子里的一颗石子 SG 值等于 $lowbit(n-i)$,那么整个题就化为(注意,最开始给的是差分):

  • 给出某些区间 $[l,r]$,求 $lowbit(l)\sim lowbit(r)$ 的异或。

容易在 $O(m\log n)$ 时间内求出,已经可以过了。

进一步讨论可以发现这就等价于最开始的结论(留作练习)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int m;
ll n;
void Solve(){
    cin>>n>>m;
    assert(2<=m),assert(m<=n),assert(m%2==0);
    ll sum=0,x,lst=0;
    for(int i=1;i<=m;i++)scanf("%lld",&x),sum^=n-x,assert(x>lst),assert(x<=n),lst=x;
    if(!sum)puts("TIN");
    else puts("NIT");
}
int main(){
    int t;
    cin>>t;
    while(t--)Solve();
}

C

当然,经过很多常数优化的标程能在时限(5s)内跑出所有数据。但是作为参赛者,你不需要关心常数!理论上,只要有一个能在合理时间内(几分钟)跑出来的做法,就能 AC 这道题,因为可以写个高精度,预处理所有答案不取模的值,然后打表。所以我们的目标就是得到一个做法,不关心时间复杂度。

考虑设计一个暴力 dp。设 $f(n,m,l,r)$ 表示有多少个符合要求的长度为 $n$ 的排列的 $m$ 元组,且逆序对数在 $[l,r]$ 之间,且逆序对数最小的恰好有 $l$ 个逆序对。转移时,将这 $m$ 个排列按照第一个元素划分为 $n$ 段,枚举每段的分界线位置的排列有几个逆序对,可以再用一个 dp 来合并。

标程的实现是 $O(n^{10}m^3)$ 的。(这个复杂度酷炫!)

感谢验题人 @Alex_Wei 的加强,他给出了一种 $O(n^8m^2)$ 的实现。

下面是标程。

#include <bits/stdc++.h>
using namespace std;
using ull = unsigned long long;
using L = __uint128_t;
constexpr int N = 15 + 5;
constexpr int M = 30 + 5;
constexpr int I = 110 + 5;
int n, m, mod, f[N][M][I][I];
struct FastMod {
  ull b, m;
  FastMod(ull b): b(b), m(ull((L(1) << 64) / b)) {}
  ull reduce(ull a) {
    ull q = ull((L(m) * a) >> 64), r = a - q * b;
    return r >= b ? r - b : r;
  }
};
void add(int &x, int y) {x += y, x >= mod && (x -= mod);}
int main() {
  #ifdef ALEX_WEI
    freopen("1.in", "r", stdin);
    freopen("1.out", "w", stdout);
  #endif
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m >> mod;
  FastMod F(mod);
  if(mod == 1) puts("0"), exit(0);
  f[1][1][0][1] = 1;
  for(int i = 2; i <= n; i++) {
    int iv = i * (i - 1) / 2 + 1;
    static int s[M][I][I];
    memset(s, 0, sizeof(s));
    for(int c = 1; c < m; c++)
      for(int r = 1; r <= iv; r++)
        for(int l = 0; l <= r; l++) {
          s[c][r][l] = f[i - 1][c][l][r];
          if(l) add(s[c][r][l], s[c][r][l - 1]);
        }
    for(int l = 0; l < iv; l++) {
      static int g[M][I], h[M][I];
      memset(g, 0, sizeof(g));
      memset(h, 0, sizeof(h));
      g[0][l] = h[0][l] = 1;
      for(int j = i - 1; ~j; j--) {
        for(int c = 0; c < m; c++)
          for(int k = 0; k < iv; k++)
            if(g[c][k])
              for(int d = 1; d + c <= m; d++)
                for(int r = max(j + 1, k + 1); r <= iv; r++) {
                  int coef;
                  if(c) {
                    coef = s[d][r - j][r - j];
                    if(k > j) add(coef, mod - s[d][r - j][k - j - 1]);
                  }
                  else {
                    if(k < j) continue;
                    coef = f[i - 1][d][k - j][r - j];
                  }
                  if(coef) add(h[c + d][r], F.reduce(1ll * g[c][k] * coef));
                }
        memcpy(g, h, sizeof(g));
      }
      for(int c = 1; c <= m; c++)
        for(int r = 1; r <= iv; r++)
          f[i][c][l][r] = g[c][r];
    }
  }
  for(int u = 1; u <= n; u++) {
    for(int t = 1; t <= m; t++) {
      int ans = 0;
      for(int l = 0; l < I; l++)
        for(int r = l + 1; r < I; r++)
          add(ans, f[u][t][l][r]);
      cout << ans << " ";
    }
    cout << "\n";
  }

  return 0;
}

D

这个题出题的时候是找规律找出来的,但我一直不会证明。

答案等于 $$ \dfrac{\prod_{S\ne \varnothing,S\subseteq \{1,2,\dots,n\}}\left(\sum_{i\in S}a_i\right)^{\prod_{i\in S}(a_i-1)}}{\prod a_i} $$ 某一天我在浏览集训队论文的时候,发现 2020 年潘佳奇论文里的一个结论直接就证明了上述公式!读者可以参考《浅谈线性代数与图论的关系》中例题 5.4.2。

具体地,我们给出的超立方体上的图,实际上是 $n$ 张完全图,分别为 $a_1,\dots,a_n$ 个点,的 Cartesian 积。而它的生成树个数,就等于 Laplace 矩阵所有非零特征值的积除以点数。而 Cartesian 积 Laplace 矩阵的特征值,就是每个图的特征值集合里取一个相加的所有可能值。注意 $x$ 个点完全图 Laplace 矩阵 的特征值集合就是一个 $0$,$x-1$ 个 $x$,这就证明了上述结论。

容易用背包计算上面的式子(体积是 $\sum a_i$,价值是 $\prod (a_i-1)$),注意背包算的是指数和,应该对 $mod-1$ 取模。时间复杂度 $O(n^2\max a_i)$。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef array<ll,2> pr;
typedef vector<int> ve;
const int mod=998244353;
int n,f[500005],prod=1,s=0;
int Power(int x,int y){
    int r=1;
    while(y){
        if(y&1)r=1ll*r*x%mod;
        x=1ll*x*x%mod,y>>=1;
    }
    return r;
}
int main(){
    cin>>n,f[0]=1;
    for(int i=1,x;i<=n;i++){
        cin>>x,prod=1ll*prod*Power(x,mod-2)%mod;
        for(int j=s;j>=0;j--)f[j+x]=(f[j+x]+1ll*f[j]*(x-1))%(mod-1);
        s+=x;
    }
    for(int i=1;i<=s;i++)prod=1ll*prod*Power(i,f[i])%mod;
    cout<<prod;
}

评论

dengruixun
都是错的

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。