2020 China Collegiate Programming Contest Qinhuangdao Site

写在前面

比赛地址:https://codeforces.com/gym/102769

以下按个人向难度排序。

我赛时写了一坨屎,硬吃三发最后扔给 Nebulyu 重构了过了,真想死。

在犹豫要不要入一个 75 配列的键盘、、、现在手里这把是高考之后买的 ikbc 的 108 键感觉好笨重啊、、、在家里大桌子上还感觉挺好用的,但在宿舍和机房都用着好憋屈啊、、、

虽然 cherry 手感和质量都没的说,但是扔在宿舍里吃灰好久了、、、

A

签到。

典中典之古典概型,输出 \(\frac{r(r - 1)}{(r + b)(r + b - 1)}\)

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
int gcd(int x_, int y_) {
  return y_ ? gcd(y_, x_ % y_) : x_;
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  int T = read();
  for (int i = 1; i <= T; ++ i) {
    printf("Case #%d: ", i);
    int x = read(), y = read();
    if (x < 2 || x + y < 2) {
      printf("0/1\n");
      continue;
    }
    int a = x * (x - 1), b = (x + y) * (x + y - 1);
    int d = gcd(a, b);
    a /= d, b /= d;
    printf("%d/%d\n", a, b);
  }
  return 0;
}

F

神 Nebulyu 直接切了,看都没看。

Code by Nebulyu:

#include<bits/stdc++.h>
#define ffor(i,a,b) for(int i=a;i<=b;++i)
using namespace std;
const int N=1e6+5;
int f[N];int g(int p){return (p^f[p]?f[p]=g(f[p]):p);}
int sp[N],se[N];
int n,m;
void solve(){
	cin>>n>>m;
	ffor(i,1,n)f[i]=i,sp[i]=1,se[i]=0;
	ffor(i,1,m){
		int p1,p2;cin>>p1>>p2;
		if(g(p1)==g(p2)){++se[g(p1)];continue;}
		int f1=g(p1),f2=g(p2);
		sp[f1]+=sp[f2],se[f1]+=se[f2];
		++se[f1],f[f2]=f1;
	}
	int ans=0;
	ffor(i,1,n)if(i==g(i))
	ans+=max(0,se[i]-sp[i]);
	cout<<ans<<"\n";
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int T;cin>>T;
	for (int i = 1; i <= T; ++ i) {
  		cout<<"Case #"<<i<<": ";
  		solve();
  	}
	return 0;
} 

G

典中典。

发现 \(\left\lfloor\sqrt[k]{x}\right\rfloor\) 至多只有 \(\sqrt{n}\) 种,且 \(k\ge 32\) 时恒有 \(\left\lfloor\sqrt[k]{x}\right\rfloor = 1\)

于是暴力枚举每一段并检查其中 \(\left\lfloor\sqrt[k]{x}\right\rfloor\) 的倍数的个数即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int n, k;
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
LL qpow(LL x_, LL y_) {
  LL ret = 1;
  while (y_) {
    if (y_ & 1) ret = ret * x_;
    x_ = x_ * x_, y_ >>= 1ll;
  }
  return ret;
}
void Solve() {
  if (k >= 32) {
    printf("%d\n", n);
    return ;
  }
  LL ans = 0, l = 0, r = 0;
  for (int i = 1; ; ++ i) {
    l = r + 1, r = std::min(qpow(i + 1, k) - 1, 1ll * n);
    ans += (r / i) - (1ll * l - 1) / i;
    if (r == n) break;
  }
  printf("%lld\n", ans);
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  int T = read();
  for (int i = 1; i <= T; ++ i) {
    printf("Case #%d: ", i);
    n = read(), k = read();
    if (k == 1) {
      printf("%d\n", n);
      continue;
    }
    Solve();
  }
  return 0;
}

E

考虑把所有可能得分升序排序,然后顺序枚举可能的最高分,及格人数即为一段滑动窗口,维护其中不同的人数即可。

注意先把滑动窗口的右端点移动到可能的最高分的最小值上。

我赛时写了一坨屎,紫砂了。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
//=============================================================
int n;
LL p;
struct AdmireVega {
  int id;
  LL val;
} a[kN << 1];
int num1, num, cnt[kN], cnt1[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
bool cmp(AdmireVega fir_, AdmireVega sec_) {
  return fir_.val < sec_.val;
}
void add(int x_) {
  num += (!cnt[x_]);
  ++ cnt[x_];
}
void sub(int x_) {
  num -= (cnt[x_] == 1);
  -- cnt[x_];
}
void Solve() {
  int ans = 0, l = 1, r = 0;
  for (int i = 1; i <= 2 * n; ++ i) {
    ++ r;
    if (!cnt1[a[r].id]) ++ num1, cnt1[a[r].id] = 1;
    add(a[r].id);

    while (l <= r && (100ll * a[l].val < p * a[r].val)) {
      sub(a[l].id), ++ l;
    }
    if (num1 == n) break;
  }
  while (r <= 2 * n) {
    ans = std::max(ans, num);

    ++ r, add(a[r].id);
    while (l <= r && (100ll * a[l].val < p * a[r].val)) {
      sub(a[l].id), ++ l;
    }
  }
  printf("%d\n", ans);
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  for (int t = 1; t <= T; ++ t) {
    printf("Case #%d: ", t);
    n = read();
    p = read();
    for (int i = 1; i <= n; ++ i) {
      int a_ = read(), b_ = read();
      a[i] = (AdmireVega) {i, a_};
      a[i + n] = (AdmireVega) {i, b_};

      cnt[i] = cnt1[i] = 0;
    }
    std::sort(a + 1, a + 2 * n + 1, cmp);
    num = num1 = 0;
    Solve();
  }
  return 0;
}
/*
1
4 99
1000000000 1000000000
999999999 1000000000
999999999 1000000000
999999999 999999999
*/

K

大力手玩发现一些结论。

  • 仅需关注如何遍历所有叶子。
  • 对于某一支军队,其行动策略是从根出发并沿一条链向下深入,过程中选择性地选择某些子树遍历,最后停在链底的叶子。
  • 如果已知某支军队最终会停在某叶子 \(x\),则可以计算在这条链上的其他叶子 \(y\),是花费 \(\operatorname{dis}(1, y)\) 再派出一支新的军队更优,还是多花费 \(2\times \operatorname{dis}(\operatorname{lca}(x, y), y)\) 让这支军队顺带着遍历了更优。则仅需比较 \(\operatorname{dis}(1, \operatorname{lca}(x, y))\)\(\operatorname{dis}(\operatorname{lca}(x, y), y)\) 的值即可。
  • 一定会有某支军队最后停在最深的叶子上。

然后想到先把一支军队扔到最深的叶子上,然后考虑其他叶子是否也在这条链上,并判断是否需要再派出一支新的。然后 Nebulyu 灵机一动——这玉藻的不就是给路径染色,然后查询到最近的染色祖先的距离吗。

于是就有了一个非常好写的做法,先将所有叶子按深度降序排序,然后依次枚举并上跳到第一个染色祖先,判断在遍历该叶节点时是在遍历被染色祖先时顺带遍历还是再派一支新的军队并累计贡献,上跳中顺带着染色。

最多上跳 \(n\) 次,总复杂度 \(O(n)\) 级别。

Code by Nebulyu:

#include<bits/stdc++.h>
#define ffor(i,a,b) for(int i=a;i<=b;++i)
#define rfor(i,a,b) for(int i=a;i>=b;--i)
using namespace std;
using ll=long long;
using pii=pair<int,int>;
const int N=5e6+5;
int f[N],rec[N];ll dis[N];
int n;
pii pr[N];
void solve(){
	rec[1]=1,f[1]=1;
	cin>>n;ffor(i,2,n)f[i]=rec[i]=dis[i]=0;
	ffor(i,2,n){
		int fa;cin>>fa;
		f[i]=fa,dis[i]=dis[fa]+1;
	}
	ffor(i,1,n)pr[i]=pii(dis[i],i);
	sort(pr+1,pr+1+n);
	ll ans=0;
	rfor(i,n,2){
		int p=pr[i].second,x=p;
//		cout<<p<<" "<<ans<<"\n";
		while(!rec[x])rec[x]=1,x=f[x];
		ll d=dis[p]-dis[x];
		ans+=min(dis[p],d*2);
	}
	cout<<ans<<"\n";
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int T;cin>>T;
	for (int i = 1; i <= T; ++ i) {
  		cout<<"Case #"<<i<<": ";
  		solve();
  	}
	return 0;
} 

J

刚开始 Nebulyu 丢给我的时候以为是牛逼计数,写完 K 之后又看了遍发现调和级数能过就成了傻逼题。

考虑枚举每段的长度 \(d\),则一共有 \(\left\lfloor \frac{n}{d} \right\rfloor\) 段完整的。若 \(n\bmod d = 0\) 则分割方案唯一,仅需关注这些段中有多少段是相同的。记第 \(i\) 种段有 \(\operatorname{cnt}_i\) 个,则段长为 \(d\) 的分割对答案的贡献为:

\[\dfrac{\left\lfloor \frac{n}{d} \right\rfloor!}{\prod\limits_i \operatorname{cnt}_i !} \]

各段之间的判重字符串哈希即可。

\(n \bmod d\not= 0\) 时,由于题目要求不完整段必须是长度为 \(n\bmod d\) 的一段,则不完整段的位置仅有 \(\left\lfloor \frac{n}{d} \right\rfloor\) 种情况。还发现当不完整段向左移动一位时仅会影响相邻的两个完整段的形态,于是考虑按顺序枚举不完整段的位置,在此过程中仍然动态地维护各种段的数量即可。

特别地还需要判断当不完整段位置相同时各完整段的形态是否相同,可以通过判断删去不完整段之后剩下的字符串是否相同实现。

然而需要大力卡常、、、用 map 朴素地写了发被 T 爆了、、、

官方给的做法中首先顺序和倒序地枚举了所有完整段并进行了离散化。显然对于不完整段的所有位置,完整段的集合总可以表示成顺序和倒序枚举的所有完整段的子集,于是避免了使用 map 记录每种段的出现次数。

另外可能需要手写哈希表、、、我跑了。

TLE Code by Nebulyu:

#pragma GCC optimize(3)


#include <bits/stdc++.h>
#define ffor(i, a, b) for (int i = a; i <= b; ++i)
#define rfor(i, a, b) for (int i = a; i >= b; --i)
using namespace std;
using ll = long long;
const int LIM = 3e5;
const int N = LIM + 5;
const ll P = 998244353;
const ll M = 227;
ll fsp(ll b, ll k = P - 2)
{
  ll r = 1;
  for (; k; k >>= 1, b = b * b % P)
    if (k & 1)
      r = r * b % P;
  return r;
}
ll powM[N], fac[N], inv[N];
ll n;
string s;
ll rec[N];
ll get_feat(ll l, ll r)
{
  if (l > r)
    return 0;
  ll len = r - l;
  ll v = ((rec[r] - rec[l - 1] * powM[len + 1] % P) % P + P) % P;
  return v;
}
unordered_map <ll, int> mp;
unordered_map <ll, bool> mp2;
void solve()
{
  cin >> s;
  n = s.size();
  ffor(i, 1, n) rec[i] = rec[i - 1] * M % P + (s[i - 1] - 'a' + 1), rec[i] %= P;
  ll ans = 0;
  for (ll d = 1; d <= n; ++d)
  {
    mp.clear(), mp2.clear();
    ll k = n / d;
    ll cur = fac[k];
    if (k * d == n)
    {
      for (ll l = 1, r = l + d - 1; r <= n; l += d, r += d)
      {
        ll v = get_feat(l, r);
        ++mp[v];
        cur = cur * inv[mp[v]] % P;
      }
      // cerr << d << " " << cur << "\n";
      ans = (ans + cur) % P;
      continue;
    }
    ll l = 1, r = d;
    for (; r <= n; l += d, r += d)
    {
      ll v = get_feat(l, r);
      ++mp[v];
      cur = cur * inv[mp[v]] % P;
    }
    ans = (ans + cur) % P;
    l -= d, r -= d;
    ll hcur = get_feat(1, r);
    mp2[hcur] = 1;
    // cerr << d << " " << cur << " " << hcur << "\n";
    // l -= d, r -= d;
    ll lt = r + 1, rt = n, r2 = n, l2 = n - d + 1;
    lt -= d, rt -= d;
    for (; l >= 1; lt -= d, rt -= d, l -= d, r -= d, l2 -= d, r2 -= d)
    {
      ll v1 = get_feat(l, r), v2 = get_feat(l2, r2);
      // cerr << "pre:" << l << " " << r << " cur:" << l2 << " " << r2
          //  << " waste:" << lt << " " << rt << "\n";
      cur = cur * mp[v1] % P;
      mp[v1] = mp[v1] - 1;
      mp[v2] = mp[v2] + 1, cur = cur * inv[mp[v2]] % P;
      hcur = (get_feat(1, lt - 1) * powM[n - rt] % P + get_feat(rt + 1, n)) % P;
      if (!mp2[hcur])
        ans = (ans + cur) % P;
      // cerr << d << " " << cur << " " << hcur << "\n";
      mp2[hcur] = 1;
    }
  }
  cout << ans << "\n";
}
void init()
{
  fac[0] = powM[0] = inv[0] = 1;
  ffor(i, 1, LIM) fac[i] = fac[i - 1] * i % P;

  
  inv[1] = 1;
  ffor(i, 2, LIM) inv[i] = 1ll * (P - P / i + P) % P * inv[P % i] % P;
  ffor(i, 1, LIM) powM[i] = powM[i - 1] * M % P;
}
signed main()
{
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  init();
  int T;
  cin >> T;
  for (int i = 1; i <= T; ++i)
  {
    cout << "Case #" << i << ": ";
    solve();
  }
  return 0;
}
/*
1
aaaaa

1
aaaaaaaaa
*/

I

一个性质是二维平面中整数域内若干向量的基向量可以写作:\((a_1, 0)\)\((a_2, a_3)\)(满足 \(a_2 < a_1\)\(a_3 > 0\)),于是考虑依次加入向量时动态地维护基向量。

具体地,对于基向量 \((a_1, 0)\)\((a_2, a_3)\),设加入的新向量为 \((a, b)\),加入后基向量变为 \((a_1', 0)\)\((a_2', a_3')\)

  • 首先考虑用 \((a_2, a_3)\)\((a, b)\) 组合出纵分量上为 0 的向量 \((a_1'', 0)\) 以更新 \((a_1, 0)\)。即有下列方程组成立:

    \[\begin{cases} a_3 x + by &= 0\\ a_2x + ay &= a_1'' \end{cases}\]

    解方程即可,则 \(a_1' = \gcd(a_1, a_1'')\)

  • 然后有:\(a_3' = \gcd(a_3, b)\)

  • 然后考虑构造 \(a_2'\)。由 \(a_3'\) 的表达式可知有下列方程组成立:

    \[\begin{cases} a_3x + by &= \gcd(a_3, b) = a_3'\\ a_2x + ay &= a_2' \end{cases}\]

    通过扩展欧几里得解出系数 \(x,y\) 后即得 \(a_2'\)。但此时可能有 \(a_2' \ge a_1'\),但这根本不是问题,直接令 \(a_2' \bmod a_1'\) 即可,等价于对基向量 \((a_2', a_3')\) 加上若干个 \((a_1', 0)\)

为了保证基向量满足上述规定的大小范围,并且防止出现 \(\bmod 0\) 的情况,代码实现时需要注意特判 \(a_1, a_2, a_3, b\) 为 0 的情况,解释起来有点麻烦,详见代码。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int q;
LL ans, a1, a2, a3;
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
void Init() {
  ans = 0;
  a1 = a2 = a3 = 0;
}
LL exgcd(LL a_, LL b_, LL &x_, LL &y_) {
  if (!b_) {
    x_ = 1, y_ = 0;
    return a_;
  }
  LL d = exgcd(b_, a_ % b_, y_, x_);
  y_ -= a_ / b_ * x_;
  return d;
}
void Add(LL a_, LL b_) {
  LL x, y;
  if (a2 == 0 && a3 == 0) {
    if (b_) {
      a2 = a_, a3 = b_;
    } else {
      a1 = exgcd(a1, a_, x, y);
    }
    return ;
  }
  LL d = exgcd(a3, b_, x, y), lcm = a3 / d * b_;
  LL k1 = lcm / a3, k2 = -lcm / b_;
  a1 = exgcd(a1, abs(k1 * a2 + k2 * a_), x, y);

  d = exgcd(a3, b_, x, y);
  a3 = d;
  a2 = ((x * a2 + y * a_) % a1 + a1) % a1;
}
bool check(LL a_, LL b_) {
  if (b_ && !a3) return false;
  if (!b_) return (a_ % a1) == 0;

  if (b_ % a3) return false;
  if (!a1) return (a_ - b_ / a3 * a2) == 0;
  return (abs(a_ - b_ / a3 * a2) % a1) == 0;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  for (int time = 1; time <= T; ++ time) {
    Init();
    q = read();
    while (q --) {
      int opt = read();
      if (opt == 1) {
        LL x = read(), y = read();
        Add(x, y);
      } else {
        LL x = read(), y = read(), w = read();
        if (check(x, y)) ans += w;
      }
    }
    printf("Case #%d: %lld\n", time, ans);
  }
  return 0;
}

B

口了,有时间就写。

写在最后

学到了什么:

  • E:复习了写了一坨屎的滑动窗口。
  • K:算是一种带反悔的贪心?
  • J:小清新哈希技巧(卡常我操你女马)。
  • I:二维平面中整数域内基向量可以写作:\((a_1, 0)\)\((a_2, a_3)\)
posted @ 2023-10-26 16:49  Luckyblock  阅读(144)  评论(10编辑  收藏  举报