CSU-XCPC2024暑假集训结训测试题解
写在前面
比赛地址:https://www.luogu.com.cn/contest/189707。
以下按预估期望难度排序。
关于出题:
满足特定条件的选手将获得由副队长提供的神必奖品一份,描述了条件和奖品的字符串为:总排名大于三,通过了题目“萨卡班班班班班班班班班班班班班班甲鱼”,且通过的时刻最晚的选手,将获得由副队长亲手制作的神必萨卡班甲鱼挂件一个
,该字符串的 sha-256 值为:26a1ae2823c939b076ea9d90d36eed1a71ee6d226f1eae2d859f88d06e043e13
。
我是一个一个一个一个签到题
签到 800
我们需要签到!
考虑对 \(a, b, c\) 拆位并依次确定答案的每一位。
容易发现当且仅当 \(a, b, c\) 该位上全为 1 或全为 0 时,答案该位上可以取 0,否则最优情况下只能取 1。
总时间复杂度 \(O(T\log v)\) 级别。
//签到
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
int a, b, c, ans = 0; std::cin >> a >> b >> c;
for (int i = 0; i <= 30; ++ i) {
if ((a >> i & 1) && (b >> i & 1) && (c >> i & 1)) continue;
if (!(a >> i & 1) && !(b >> i & 1) && !(c >> i & 1)) continue;
ans += (1 << i);
}
std::cout << ans << "\n";
}
return 0;
}
/*
4
1 1 4
5 1 4
5 2 5
1 7 0
*/
小橙子
签到 800
把Y看作1,X看作0,一次操作就是二进制意义下-1
答案就是二进制意义下的数值。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 60;
int n;
char str[N];
LL t=1;
int main () {
scanf("%d%s", &n, str);
LL ans=0;
for (int i=0; i<n; i++) if (str[i]=='Y') {
ans+=(t<<i);
}
cout<<ans<<endl;
return 0;
}
启动法阵
数学,枚举 1300
首先注意到答案是两两左右对称的,我们可以只考虑所有斜率>0的线段:考虑枚举构成线段的两条直角边\(x,y\)。可得到两个限制条件:
长度限制:\(L1\le sqrt(x^2+y^2) \le L2\)
无阻挡限制:\(gcd(x,y)=1\)。
得到一组满足条件的\(x,y\)后,易得它的贡献为\(2*(n-x+1)*(m-y+1)\)。
此外,我们要注意水平竖直的线段,它们的对称是同一个,而且只有长度为1时才符合要求。单独处理即可。
#include <bits/stdc++.h>
using namespace std;
long long ans,n,m,l,r;
int gcd(int a,int b){
return b?gcd(b,a%b):a;
}
void solve(){
cin>>n>>m>>l>>r;
if(l==1){ans=ans+(n+1)*m+(m+1)*n;}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
double now=sqrt(i*i+j*j);
if(now<l||now>r||gcd(i,j)!=1)continue;
ans=ans+2*(n-i+1)*(m-j+1);
}
}
cout<<ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int T=1;
//cin>>T;
while(T--){
solve();
}
return 0;
}
彧
枚举,单调性 1400
令 \(f(i, j) = i \cdot j - k \cdot (a_i | a_j)\),其中 \(i < j\)。
在这个公式中,\(i \cdot j\) 可能是 \(\mathcal{O}(n^2)\),但 \(k \cdot (a_i | a_j)\) 是 \(\mathcal{O}(n \cdot 100)\)。这意味着对于较大的 \(i, j\),\(f(i, j)\) 的值一定更大。
哪个最小的 \(i\) 可以对结果(\(f(i, j): i < j\) 是最大值)做出贡献?尝试最大化 \(f(i, j)\) 并最小化最重的配对,即 \(f(n - 1, n)\),然后将它们进行比较。
包含 \(i\) 的配对可能的最大值是什么?当 \(i\) 与 \(n\) 配对且 \(a_i = a_n = 0\) 时,它的值最大。因此,\(f(i, n) = i \cdot n - k \cdot 0 = i \cdot n\)。
最大配对 \(f(n - 1, n)\) 可能的最小值是多少?当 \(a_{n - 1} | a_n\) 最大时,值最小。而且,由于 \(0 \le a_i \le n\),任何 \(a_i | a_j\) 的最大可能值是 \(\le 2n\)。所以 \(f(n - 1, n) = (n - 1) \cdot n - k \cdot 2n = n^2 - 2kn - n\)。
为了使 \(i\) 对结果有所贡献,\(f(i, n)\) 必须大于 \(f(n - 1, n)\)。并且,当 \(f(i, n) > f(n - 1, n)\) 时,那么 \(i \cdot n > n^2 - 2kn - n\),或者 \(i > n - 2k - 1\)。
因此,任何 \(f(i, j)\) 满足 \(i < n - 2k\) 都不会产生大于 \(f(n - 1, n)\) 的值!这表明我们只需要检查满足 \(i, j \ge n - 2k\) 的配对 \(f(i, j)\)。而且,这样的配对只有 \(\mathcal{O}(k^2)\),因此我们可以用暴力搜索。
我们也允许 \(\mathcal{O}(n \cdot k)\) 的解决方案通过,即暴力搜索所有满足 $1 \le i \le n $ 且 \(n - 2k \le j \le n\) 的配对。
时间复杂度: \(\mathcal{O}(k^2)\)。
#include<bits/stdc++.h>
using namespace std;
int32_t main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
int t; cin >> t;
while (t--) {
int n, k; cin >> n >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
long long ans = -1e12;
int l = max(1, n - 2 * k);
for (int i = l; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
ans = max(ans, 1LL * i * j - 1LL * k * (a[i] | a[j]));
}
}
cout << ans << '\n';
}
return 0;
}
如何优雅地卡 Spfa
最小生成树 1800
我会直接建图跑 kruscal!复杂度 \(O(nm\log nm)\) 铁过不去呃呃。
考虑直接做 kruscal 进行时,会依次选择哪些边加入最小生成树。
首先边权值最小的一行/一列一定会被全部加入最小生成树;同行/列的边权相等,则按排序后它们一定相邻。当枚举到它们时,若它们连接的点此时全部不连通,则它们会被全部添加到最小生成树中,否则仅会选择加边前不连通的点之间的边进行添加。
加边后一定会使这一行/列上的点被全部加入到最小生成树中。于是将所有行/列按照权值排序,仅需考虑加入一整行/一整列时每次可以加入多少边;由上分析手玩下容易发现在尝试加入某一行/列时,实际上添加的边数即这一行/列的点数,减去此时已经被添加到生成树中的点数。
于是仅需在加入整行整列的同时,维护下添加边时已被加入的行/列数即可。复杂度 \(O((n+m)\log (n+m))\) 级别。
//知识点:最小生成树
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e5 + 10;
//=============================================================
int n, m, a[kN], b[kN], linked[2];
LL ans;
//=============================================================
//=============================================================
int main() {
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
for (int j = 1; j <= m; ++ j) std::cin >> b[j];
std::sort(a + 1, a + n + 1), std::sort(b + 1, b + m + 1);
ans += 1ll * (m - 1) * a[1] + 1ll * (n - 1) * b[1];
++ linked[0], ++ linked[1];
for (int i = 2, j = 2; i <= n && j <= m; ) {
if (a[i] <= b[j]) {
ans += 1ll * (m - linked[1]) * a[i];
++ linked[0], ++ i;
} else {
ans += 1ll * (n - linked[0]) * b[j];
++ linked[1], ++ j;
}
}
std::cout << ans << "\n";
return 0;
}
可曾记得爱?
手玩,思维,结论 1800
如果你敢大力手玩样例二就直接秒了。
有结论:对于任意一种划分方案 \((A, B)\),它们的价值 \(v(A, B)\) 均相等。则考虑将给定数列 \(a\) 排序,取前 \(n\) 大作为 \(A\),前 \(n\) 小作为 \(B\),再乘上划分的方案数,答案即为:
考虑证明。设给定数列 \(a\) 的前 \(n\) 小元素集合为 \(L\),前 \(n\) 大元素集合为 \(R\)。对于任意一种划分方案中对应位置的元素 \(A_i, B_i\),由 \(A\) 升序 \(B\) 降序的性质可知,原数列中有 \(i-1\) 个元素不大于 \(A_i\),有 \(n-i\) 个元素不大于 \(B_i\)。
则若 \(A_i, B_i\) 同时属于集合 \(L\),可推导出集合 \(L\) 的大小 \(|L| \ge (i - 1) + (n - i) + 2 = n + 1\),与定义矛盾,则 \(A_i, B_i\) 一定不同时属于 \(L\);同理可证得 \(A_i, B_i\) 不同时属于 \(R\)。
则对于任意一种划分方案中对应位置的元素 \(A_i, B_i\),一定有:一方属于前 \(n\) 小元素集合 \(L\),另一方属于前 \(n\) 大元素集合 \(R\)。则可知任意划分方案的价值:
实现起来非常简单,总时间复杂度 \(O(n)\) 级别。
PS:本来没有中样例的,因为出题组人美心善害怕大家不敢手玩后来就加上去了,实属人间之鉴!
//知识点:手玩,思维,结论
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 4e5 + 10;
const int p = 1e9 + 7;
//=============================================================
int n, a[kN];
LL ans, fac[kN];
//=============================================================
LL qpow(LL x_, LL y_) {
LL ret = 1;
while (y_) {
if (y_ & 1) ret = ret * x_ % p;
x_ = x_ * x_ % p, y_ >>= 1ll;
}
return ret;
}
LL C(int n_, int m_) {
fac[1] = 1;
for (int i = 2; i <= n_; ++ i) fac[i] = 1ll * fac[i - 1] * i % p;
return fac[n_] * qpow(fac[m_], p - 2) % p * qpow(fac[n_ - m_], p - 2) % p;
}
//=============================================================
int main() {
std::cin >> n;
for (int i = 1; i <= 2 * n; ++ i) std::cin >> a[i];
std::sort(a + 1, a + 2 * n + 1);
for (int i = 1; i <= n; ++ i) ans = (ans - a[i] + p) % p;
for (int i = n + 1; i <= 2 * n; ++ i) ans = (ans + a[i]) % p;
std::cout << ans * C(2 * n, n) % p;
return 0;
}
/*
1
2 3
3
1 1 4 5 1 4
5
525170 448688 366276 418613 369403 478374 566746 482287 496179 565781
*/
萨卡班班班班班班班班班班班班班班甲鱼
线段树 1900
典中典之套路线段树,为了让大家不会中间下班最后补的码量题。
发现询问显然可以分治解决。考虑将询问区间分成左右两半部分,分别求得左右部分中的完整萨卡班甲鱼数,则仅需考虑横跨区间的至多一条萨卡班甲鱼即可。发现两个区间合并后可以拼出一条萨卡班甲鱼当且仅当:
- 左区间的后缀为:
(===...
形式。 - 右区间的前缀为:
...===<
形式。
则线段树维护以下信息即可完成单点修改和区间查询操作:
- 区间是否全部为
=
; - 区间的后缀是否为
(===...
形式; - 区间的前缀是否为
...===<
形式; - 区间内完整萨卡班甲鱼数量。
总时间复杂度 \(O((n + q)\log n)\) 级别。
分享一种很方便的需要合并复杂信息的线段树写法,考虑将每个节点的信息存入结构体,并实现一个合并两个结构体并返回合并后的函数,则查询时仅需将先后查到的区间对应的结构体直接调用函数合并起来即可,hdu 第三场我就是用的这个写法,比大力讨论好写一万倍。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, m, value[210];
std::string s;
//=============================================================
namespace seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
const int kNode = kN << 2;
struct node {
bool allbody, have[2];
int cnt;
} t[kNode];
node init(int val_) {
node ret = (node) {(val_ == 2), 0, 0, 0};
if (val_ < 2) ret.have[val_] = 1;
return ret;
}
node merge(const node x_, const node y_) {
node ret;
ret.allbody = x_.allbody && y_.allbody;
ret.have[0] = (x_.have[0] || (x_.allbody && y_.have[0]));
ret.have[1] = (y_.have[1] || (y_.allbody && x_.have[1]));
ret.cnt = x_.cnt + y_.cnt + (x_.have[1] && y_.have[0]);
return ret;
}
void pushup(int now_) {
t[now_] = merge(t[ls], t[rs]);
}
void build(int now_, int L_, int R_) {
if (L_ == R_) {
t[now_] = init(value[(int) s[L_]]);
return ;
}
build(ls, L_, mid), build(rs, mid + 1, R_);
pushup(now_);
}
void modify(int now_, int L_, int R_, int pos_, int val_) {
if (L_ == R_) {
t[now_] = init(val_);
return ;
}
if (pos_ <= mid) modify(ls, L_, mid, pos_, val_);
else modify(rs, mid + 1, R_, pos_, val_);
pushup(now_);
}
node query(int now_, int L_, int R_, int l_, int r_) {
if (l_ <= L_ && R_ <= r_) return t[now_];
if (r_ <= mid) return query(ls, L_, mid, l_, r_);
if (l_ > mid) return query(rs, mid + 1, R_, l_, r_);
return merge(query(ls, L_, mid, l_, r_), query(rs, mid + 1, R_, l_, r_));
}
int query(int l_, int r_) {
return query(1, 1, n, l_, r_).cnt;
}
#undef ls
#undef rs
#undef mid
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
value['<'] = 0, value['('] = 1, value['='] = 2;
std::cin >> n >> m;
std::cin >> s; s = "$" + s;
seg::build(1, 1, n);
while (m --) {
int opt; std::cin >> opt;
if (opt == 1) {
int x; char c; std::cin >> x >> c;
seg::modify(1, 1, n, x, value[(int) c]);
} else {
int l, r; std::cin >> l >> r;
std::cout << seg::query(l, r) << "\n";
}
}
return 0;
}
图的统计
图论,计数 2000
注意到合法的图中最小生成树唯一且一定为给定的树,则可知对于任意合法的图的任意子图,其最小生成森林一定也是给定的最小生成树的子图。且连接这些子图的会出现在最小生成树中的边,一定是给定的最小生成树中连接各个森林的边。
于是一个想法是考虑枚举给定的最小生成树的边,在此过程中维护已构造出的最小生成森林,然后仅考虑每次加入一条最小生成树的边时,这条边连接的两个森林之间的边有哪些可能,即求在每加入一条树边时,可额外加入的非树边的方案数。
考虑对于一个确定的图中的最小生成树,若想要通过加边使得最小生成树变得更小,则只需要找到树上最大的边,并将连接了最大边的两个连通块的,任意一条小于该长度的边加入图中即可,可以使得加入这条边并删掉最大边后该树仍需联通。由上可知,若想要新加入的边无法影响原先的最小生成树,新加入的边必须比上文提到的最大边要大,且新边必须连接最大边连接的两个连通块。
于是考虑此时枚举到的最大的树边连接的连通块分别为 \(u, v\),最大边权值为 \(w\),则新加入的边可以任意连接两个点集中的点,且边权值的范围为 \([w + 1, S]\)。则那么新加入边的方案数则为 \((S-w+1) ^ {\operatorname{size}(u)\times \operatorname{size}(v)- 1}\)(减的一代表一定存在的最长的树边)。
于是考虑从小往大枚举树边,逐一进行上述操作并求得每加一条树边时能额外加非树边的方案数即可。
#include <bits/stdc++.h>
#define pii pair<int, int>
#define vi vector<int>
#define vll vector<long long>
#define vpii vector<pair<int, int>>
#define mp make_pair
#define fi first
#define se second
#define all(v) v.begin(), v.end()
#define pb push_back
#define endl "\n"
using namespace std;
using LL = long long;
using ULL = unsigned long long;
const int N = 2e5 + 10, INF = 0x3f3f3f3f, MOD = 998244353;
struct DSU
{
std::vector<int> f, siz;
DSU() {}
DSU(int n)
{
init(n);
}
void init(int n) // 初始化
{
f.resize(n); // 将f的容量设为n
std::iota(f.begin(), f.end(), 0); // 将f[i]初始化为i
// iota:为一段连续的空间赋连续的值
siz.assign(n, 1); // 将每个连通块的初始大小定为1 siz[i]表示点i所在连通块大小
}
int leader(int x) // 求出点x所在连通块中的代表元
{
while (x != f[x])
{
x = f[x] = f[f[x]];
}
return x;
}
bool same(int x, int y) // 判断x和y是否在同一连通块
{
return leader(x) == leader(y);
}
bool merge(int x, int y) // 将x,y所在连通块合并
{
x = leader(x);
y = leader(y);
if (x == y)
{
return false;
}
siz[x] += siz[y];
f[y] = x;
return true;
}
int size(int x)
{
return siz[leader(x)];
}
};
LL qmi(LL a, LL b)
{
LL res = 1, mul = a;
while (b)
{
if (b & 1)
res = res * mul % MOD;
mul = mul * mul % MOD;
b >>= 1;
}
return res;
}
void Sol()
{
int n, S;
LL ans = 1;
cin >> n >> S;
vector<tuple<int, int, int>> e(n);
for (int i = 0; i < n - 1; i++)
{
int u, v, w;
cin >> u >> v >> w;
u--, v--;
e[i] = {w, u, v};
}
sort(all(e));
DSU dsu(n);
for (auto [w, u, v] : e)
{
ans = (ans * (qmi(S - w + 1, (LL)dsu.size(u) * dsu.size(v) - 1))) % MOD;
dsu.merge(u, v);
}
cout << ans << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
{
Sol();
}
return 0;
}
前后缀
枚举,单调性 2000
观察要计算的式子,是先枚举区间再枚举断点,再计算两端的最大前缀和最大后缀。
但是这样很难批处理,考虑变换求和顺序,改为先枚举断点,对于每个断点,需要取遍左端点、取遍右端点并将结果相乘。再每个断点处将左边每个区间的最大前缀和乘右边每个区间的最大后缀和即可。
考虑如何预处理,以下只讨论前缀和,后缀和同理。
考虑新加入一个数的贡献。之前的所有区间要么维持原本的最大前缀,要么在原本最大前缀的基础上多取一段,一直取到新加的数。多取一段的条件是:这一段的和大于等于零。
观察一:对于所有选择多取一段的区间,再下次判断时,他们的原本最大前缀都是当前数。也就是说对于之后的新数,他们的判断条件相同。意味着这一段区间可以合并为一个元素进行判断,只需要维护区间个数。
观察二:选择是单调的,一旦某个区间选择维持原本的最大前缀,这个区间之前的区间都会维持原本的最大前缀。反证法即可,维持说明新加数到原本最大前缀的区间和小于零,那么再之前的区间取当前数也是不优的。
那么我们只需要维护一个栈,将所有最大前缀结尾相同的区间记为同一个元素,需要记录最大前缀的结尾位置和这样的区间数量。
每次加入一个数时,这个数本身构成区间数量为1、最大前缀结尾为它本身的元素。再依次检查栈顶元素,判断是否最大前缀能否更新到这个数,如果可以就将栈顶元素合并到当前元素。否则就停止取出并将当前元素入栈。在维护这个栈的同时维护当前断点的最大前缀和即可。
栈内元素只会被插入删除一次,时间复杂度 O(n)。当然少观察一些性质并写一些数据结构应该有 O(nlogn) 的做法,不过所有验题人都是直接写的线性做法,所以我们并不会怎么带 log(
#include<bits/stdc++.h>
#define int long long
#define x first
#define k second
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10,mod=1e9+7;
PII stk[N];
int top;
int a[N];
int L[N],R[N];
int n;
void add(int &a,int b)
{
if(a+b>=mod) a=a-mod+b;
else a=a+b;
}
signed main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
stk[++top]={0,1};
for(int i=1,sum=0,ans=0;i<=n;i++)
{
sum+=a[i];
int cnt=0;
while(top && sum-stk[top].x>=0) add(ans,(sum-stk[top].x)%mod*stk[top].k%mod),cnt+=stk[top].k,top--;
stk[++top]={sum,1+cnt};
L[i]=ans;
}
top=0;
stk[++top]={0,1};
for(int i=n,sum=0,ans=0;i>=1;i--)
{
sum+=a[i];
int cnt=0;
while(top && sum-stk[top].x>=0) add(ans,(sum-stk[top].x)%mod*stk[top].k%mod),cnt+=stk[top].k,top--;
stk[++top]={sum,1+cnt};
R[i]=ans;
}
int res=0;
for(int i=1;i<n;i++) add(res,L[i]*R[i+1]%mod);//res+=L[i]*R[i+1];
cout<<res<<"\n";
return 0;
}
xormex
数据结构,Trie 2100
考虑用\(01Trie\)存储数。考虑一颗\(Trie\)对应的\(xormex\)值。
那么先考虑\(mex\)怎么求。从根出发,如果左子树(\(0\)边)满了就把左子树的大小加入答案(大小为\(2\)的幂),进入右子树,如果左子树没满就进入左子树。如果为叶子结点,就答案\(+1\)。
异或就是交换\(0,1\),也就是可以某一深度的所有结点交换左右子树,那么只要把原来的求\(mex\)方法小小改动一下即可。
-
如果一棵树的两颗子树至少一棵树是满的(完全二叉树),此树的DP值就是两颗子树的DP值和(选取两颗子树)。
-
如果两棵子树都不满,那么此树的DP值就是两颗子树的DP值中较大的(选取一颗子树)。
-
叶子DP值为 1
关于正确性,可以发现,每一层最多选取了一个未满的子树,而已经满的子树怎么交换仍然是满的,所以只要用异或使得唯一未满的子树得到最优解即可,不会冲突。
每次加入一个数就是加入一个叶子结点,更新叶子到根上的节点的DP值即可。询问只要取出来根的DP值就行。
复杂度\(O(n\cdot 2^n)\)
注解:因为要不断地根据子树的 \(\operatorname{size}\) 向上合并信息,实际实现起来比起 trie 更像一棵长度为 \(2^n\) 的线段树。江队的 std 为了追求极致的优美直接使用了线段树并且是非递归线段树(即 zkw 线段树)实现。按照上述 trie 的思路大力实现可见这发提交:https://www.luogu.com.cn/paste/wfihed0n。
#include<bits/stdc++.h>
typedef int LL;
const signed maxn=(1<<20)+5;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=1;
for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
if(f) x=-x;return x;
}
int tr[maxn<<1],ans[maxn],ar[maxn];
void change(int pos,int w){
int bit=1;
tr[pos]=w;pos>>=1;
while(pos){
if(tr[pos<<1|1]==bit||tr[pos<<1]==bit)
tr[pos]=tr[pos<<1|1]+tr[pos<<1];
else tr[pos]=std::max(tr[pos<<1],tr[pos<<1|1]);
pos>>=1;bit<<=1;
}
}
signed main(){
int n=1<<Read();
for(int i=1;i<=n;++i) ar[i]=Read();
for(int i=1;i<=n;++i){
change(n+ar[i],1);
printf("%d%c",tr[1]," \n"[i==n]);
}
return 0;
}
向阳绽放 少女们的小夜曲
组合数学,容斥 2100
首先把限制 \(k\) 搞掉,记 \(f(x)\) 为仅允许有长度不大于 \(x\) 的全 1 段的方案数,则限定最长段为 \(k\) 的方案数即为 \(f(k) - f(k-1)\)。
然后考虑如何求 \(f(k)\)。限定串中有 \(m\) 个 1,即有 \(n-m\) 个 0,则构造字符串等价于在 \(n-m\) 个 0 之间和两端共 \(n-m+1\) 个空中填入总共 \(m\) 个 1,每个空可填入 \(0\sim k\) 个 1。这是个经典问题,参考 hdu6397,考虑容斥消去填入上限的限制。
设 \(g(i)\) 表示总共填入了 \(m\) 个 1 且没有填入上限,有至少 \(i\) 个空至少填入了 \(k+1\) 的方案数:
- 显然有 \(0\le i\le \min\left( n - m + 1, \frac{m}{k + 1}\right)\)。
- 对于 \(i=0\),即每个空没有填数上限,则直接插板法,方案数为 \({{(n-m+1) + m - 1} \choose {n-m-1 + 1}} = {n\choose {n-m}}\)。
- 对于 \(i>0\),考虑先选出 \(i\) 个空为它们预分配 \(k+1\),然后转化为了 \(i=0\) 的情况,方案数为 \({{n-m+1}\choose i}\times {{n - (k+1)\times i} \choose {n-m}}\)。
则根据容斥原理,若没有任何空至少填入了 \(k+1\) 的方案即 \(f(k) = \sum\limits_{0\le i} (-1)^i\times g(i)\),注意特判 \(f(-1) = 0\)。
预处理下阶乘和逆元,总时间复杂度 \(O(n)\) 级别。
//知识点:组合数学,容斥
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const LL p = 998244353;
//=============================================================
int n, m, k;
LL inv[kN], fac[kN], ifac[kN];
//=============================================================
LL C(LL n_, LL m_) {
if (m_ > n_) return 0;
return fac[n_] * ifac[m_] % p * ifac[n_ - m_] % p;
}
void Init() {
inv[1] = fac[0] = fac[1] = ifac[0] = ifac[1] = 1;
for (int i = 2; i < kN; ++ i) {
inv[i]= 1ll * (p - p / i + p) % p * inv[p % i] % p;
fac[i] = fac[i - 1] * i % p;
ifac[i] = ifac[i - 1] * inv[i] % p;
}
}
LL f(LL k_) {
if (k_ == -1) return 0;
LL ans = 0, f = 1;
for (int i = 0; i <= std::min(n - m + 1ll, m / (k_ + 1)); ++ i, f = -f) {
LL d1 = C(n - m + 1, i), d2 = C(n - (k_ + 1) * i, n - m);
ans = (ans + f * d1 * d2 % p + p) % p;
}
return ans;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
Init();
std::cin >> n >> m >> k;
std::cout << (f(k) - f(k - 1) + p) % p << "\n";
return 0;
}
圣猫福音-白猫的立方
构造 2200
参考文献:
这道题是2023CCPC秦皇岛构造题改:
构造一个一维数组\((0-indexed)\),长度\(len\)满足\(len\&1=1\&\&len\ge2^n\),元素值域为\([0,2^{n+1})\),首项为0,任意相邻项相与为0,记这个数组为\(base\),那么我们需要的数组\(a\)可以如此构造
\(a_{i,j,k}=(base[(i+j-k)\%len]<<2*n)+(base[(i-j+k)\%len]<<n)+base[(-i+j+k)\%len]\)
base构造方法:
//感觉讲不明白,直接上代码感受
ar[1]=0;ar[2]=1;ar[3]=2;
int len=3;
for(int i=2;i<=n;++i){
for(int j=1;j<len;++j) ar[2*len-j]=ar[j];
for(int j=2;j<len;j+=2) ar[j]|=(1<<i);
for(int j=1;j<len;j+=2) ar[2*len-j]|=(1<<i);
len=2*len-1;
}
首先显然,一个元素是三段base二进制数拼接在一起的,由构造显然可以得到相邻相与为\(0\)。
关于不重复,相等即三个base都相等,可以证明,当\(len\)为奇时,如果\((i+j-k)\%len,(i-j+k)\%len,(-i+j+k)\%len\)确定,\(i\%len,j\%len,k\%len\)也确定。而\(i,j,k\le 2^n<len\),说明一个数可以唯一确定一个位置,不会有两个位置有相同数的可能。
#include<bits/stdc++.h>
#define ll long long
#define pii std::pair<int,int>
typedef int LL;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=0;
for(;isdigit(ch);ch=getchar()) x=(x<<3)+(x<<1)+(ch^48);
if(f) x=-x;return x;
}
int ar[105];
std::map<long long,bool> map;
int arr[35][35][35];
signed main(){
int n=Read();
ar[1]=0;ar[2]=1;ar[3]=2;
int len=3;
for(int i=2;i<=n;++i){
for(int j=1;j<len;++j) ar[2*len-j]=ar[j];
for(int j=2;j<len;j+=2) ar[j]|=(1<<i);
for(int j=1;j<len;j+=2) ar[2*len-j]|=(1<<i);
len=2*len-1;
}
//for(int i=1;i<=len;++i) printf("%d%c",ar[i]," \n"[i==len]);
int nn=(1<<n);
for(int i=1;i<=nn;++i){
for(int j=1;j<=nn;++j){
for(int k=1;k<=nn;++k){
ll x=ar[(i+j-k+len)%len+1];
x<<=(n+1);
x+=ar[(i-j+k+len)%len+1];
x<<=(n+1);
x+=ar[(-i+j+k+len)%len+1];
arr[i][j][k]=x;
//printf("<%d,%d,%d>",(i+j-k+len)%len,(i-j+k+len)%len,(-i+j+k+len)%len);
printf("%lld ",x);
}
}
printf("\n");
}
return 0;
}
树上的研究
树剖,线段树 2300
发现直接查询路径上黑边数量不太好维护,考虑能否转成查询路径上的点的信息。由题目中的操作可知,对于每一条黑边,其两端点一定被同时被某一次操作的路径覆盖,且在这次操作后,这两个端点均没有被操作过——即覆盖了两个点的最后一次操作的编号相同的。
则每一次操作相当对路径上所有节点打时间戳,查询相当于查询路径上相邻两节点有多少对满足时间戳相等,大力树链剖分+线段树维护即可,线段树维护的区间信息合并时需要特别注意端点处新增的贡献,需要一定的实现能力,此处不再赘述。
此题定位和做法均与萨卡班班班班班班班班班班班班班班甲鱼类似但是有些脱线了。虽然可能大概还是比较板的但是场上真的会有人写?存疑。
#include<bits/stdc++.h>
#define ll long long
#define mid (l+r>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
const signed maxn=1e5+5;
typedef int LL;
inline LL Read(){
LL x=0;char ch=getchar();bool f=0;
for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=1;
for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
if(f) x=-x;return x;
}
int n,m;
struct Edge{
int to,nxt;
}edge[maxn<<1];
int head[maxn],ecnt;
void adde(int u,int v){
edge[++ecnt]=Edge{v,head[u]};
head[u]=ecnt;
}
struct Tr{
int ts,bs,sum;
Tr(){
ts=bs=sum=0;
}
Tr(int a,int b,int c):ts(a),bs(b),sum(c){}
}tr[maxn<<2];
int lazy[maxn<<2];
inline Tr operator +(const Tr&l,const Tr &r){
return Tr(r.ts,l.bs,l.sum+r.sum+(l.ts==r.bs&&l.ts!=0));
}
void update(int u){
tr[u]=tr[u<<1]+tr[u<<1|1];
}
void pushown(int u,int l,int r,int w){
lazy[u]=w;
tr[u].sum=r-l;tr[u].bs=tr[u].ts=w;
}
void pushdown(int u,int l,int r){
pushown(lson,lazy[u]);
pushown(rson,lazy[u]);
lazy[u]=0;
}
void changen(int u,int l,int r,int ql,int qr,int w){
if(ql<=l&&r<=qr) return void(pushown(u,l,r,w));
if(lazy[u]) pushdown(u,l,r);
if(ql<=mid) changen(lson,ql,qr,w);
if(qr>mid) changen(rson,ql,qr,w);
update(u);
}
Tr queryn(int u,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return tr[u];
if(lazy[u]) pushdown(u,l,r);
if(qr<=mid) return queryn(lson,ql,qr);
if(ql>mid) return queryn(rson,ql,qr);
return queryn(lson,ql,qr)+queryn(rson,ql,qr);
}
int fa[maxn],siz[maxn],son[maxn],dep[maxn];
void dfs1(int u,int Fa){
fa[u]=Fa;siz[u]=1;son[u]=0;dep[u]=dep[Fa]+1;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==Fa) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
int top[maxn],seg[maxn],anseg[maxn],scnt;
void dfs2(int u){
seg[u]=++scnt;anseg[scnt]=u;
if(son[u]) top[son[u]]=top[u],dfs2(son[u]);
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa[u]||v==son[u]) continue;
top[v]=v;dfs2(v);
}
}
inline int LCA(int u,int v){
while(top[u]!=top[v]){
if(dep[u]<dep[v]) std::swap(u,v);
u=fa[top[u]];
}
if(dep[u]<dep[v]) std::swap(u,v);
return v;
}
void P1(int u,int v,int ver){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) std::swap(u,v);
changen(1,1,n,seg[top[u]],seg[u],ver);
u=fa[top[u]];
}
if(dep[u]<dep[v]) std::swap(u,v);
changen(1,1,n,seg[v],seg[u],ver);
return;
}
int P2(int u,int v){
Tr au,av;
while(top[u]!=top[v]){
if(dep[top[u]]>dep[top[v]]){
au=queryn(1,1,n,seg[top[u]],seg[u])+au;
u=fa[top[u]];
}
else{
av=queryn(1,1,n,seg[top[v]],seg[v])+av;
v=fa[top[v]];
}
}
if(dep[u]>=dep[v]){
au=queryn(1,1,n,seg[v],seg[u])+au;
}
else if(dep[u]<dep[v]){
av=queryn(1,1,n,seg[u],seg[v])+av;
}
return (au.sum+av.sum+(au.bs==av.bs&&au.bs!=0));
}
int ans[maxn],acnt;
signed main(){
LL T=1;
while(T--){
acnt=0;
n=Read(),m=Read();
for(int i=1;i<n;++i){
int u=Read();
adde(u,i+1);adde(i+1,u);
}
dfs1(1,0);
top[1]=1;
dfs2(1);
int t=0;
for(int i=1;i<=m;++i){
int u=Read(),v=Read();
ans[i]=P2(u,v);
P1(u,v,i);
}
for(int i=1;i<=m;++i) printf("%d\n",ans[i]);
}
return 0;
}
Bonus:意义的皮夹子
原题目标题:Mean_Wallet
考虑贪心,如果不考虑\(y\)的范围,直接选取最大的\(m-k+1\)个即可。
但是\(y\)有取值范围限制,所以在选取策略上,除了优先选最大的,还要尽量留给别的操作更优的局面。
所有的操作的区间都是前一次操作区间的整体右移一列,考虑用滑动窗口式的方式解决。
保留更优局面就是在滑动窗口方式下就是窗口维护的最值尽量大。
首先从第\(1\)列到第\(m\)列开始逐列读取,假设读到第\(i\)列,\(1\le i\le k\)时,唤醒第\(i\)次操作,然后读取第\(i\)列,记已唤醒操作数量为\(c\),维护前\(c\)大的数(及其所在列),其余数绝对不会被选取。当\(k\le i\le m\)时,结束已经读取完毕的操作。结束操作即从维护的最值中选取最大值,弹出最为本操作的选择(同时更新\(c\))。
这么做肯定是很贪,但是似乎忽略了区间的左端点限制,正确性如何保证?
要证明得用到图论,要把操作和数抽象成二分图,操作可以选择的数就是一条连边。
霍尔定理:二部图G中的两部分顶点组成的集合分别为X, Y,G存在完美匹配的充分必要条件是X中的任意k个点至少与Y中的k个点相邻。\((1\le k\le |X|)\)
我们以最终选取的点集为\(X\),操作集为\(Y\),本题由于特殊的建边条件,如果我们按列对点进行排序,可以发现操作和一个区间内的所有数连边。所以我们只需要考虑按列排序后连续的数相连的区间数是否不低于数的数量。
- 为什么只要考虑按列连续点区间即可?
- 证明一个性质:如果由排序后连续的数与和它们相连的操作构成的二分图存在大小为“数的个数”的匹配,那么删去若干个数后任然存在大小为“数的个数”的匹配。
如果我们从连续的数之间删去一些数,可能会有以下结果:- 相连的操作数不变,整体仍然是一个连通图,那么这种情况减少了点数不减操作数,肯定存在匹配。
- 操作数变少了,由于"区间式的连边","消失"的操作左右两端一定不连通,操作和数之间抽象形成的图不再连通,只要考虑子图是否拥有这个性质即可。这时候套一个数学归纳法即可,子图一定比原图小,然后最小情况点只有一个数又肯定满足。 归纳一下可知正整数个数一定满足性质。
而且,可以认为操作和数都和先和列匹配,所以只要证明任意\([l,r]\)列中选取的数小于等于同列\([l,r]\)有交的操作区间的数量即可。
可以发现当读取\(l\)列时操作区间\(r'<l\)的已经选取完毕,不会贡献滑动窗口大小,\(l'>r\)的也不会开始读取,所以最后在滑动窗口中出现的\([l,r]\)内的数一定小于等于操作区间数。
以上是用霍尔定理严格证明。事实上对于取消左端点限制,有一个\(trick\)可以帮助感性理解。
\(A=\{2,3,1\}\),直接选最大值,在置零的方法就是先取\(3\),\(A'=\{2,0,1\},\{0,1\}\)中再取\(1\),但是答案显然是\(2,3\)为\(5\)。所以可以设计一个撤销方案:对于操作\(1\)而言,如果后续操作要选\(3\),操作\(1\)就选回\(2\)。凭借这个可以等价做到操作\(2\)也能选取数值\(2\)。所以区间左端点限制可以通过撤销法消去。
当然,本质是依靠霍尔定理,所以维护的最值个数得和匹配的操作数数量严格一致。
原题是 \(codeforces\) 的 \(Digital\ Wallet\),(原题\(totorial\)给的\(DP\)解法无法通过此题)题目链接https://codeforces.com/contest/1866/problem/D
我的提交:https://codeforces.com/contest/1866/submission/264666425
std有单独文件。
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define ll long long
typedef int LL;
#define pii std::pair<int,int>
#define MK(a,b) std::make_pair(a,b)
const int maxn=(int)2e5+5;
inline LL read(){
LL x=0;char ch=getchar();bool f=0;
for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=1;
for(;isdigit(ch);ch=getchar()) x=(x<<3)+(x<<1)+(ch^48);
if(f) x=-x;return x;
}
LL n,m,k;
int ar[maxn];
int rk[maxn],pos[maxn],top;
ll ans=0;
std::multiset<int> p;
int siz;
void add(int x){
if(siz==0) return;
if(p.size()<siz) p.insert(x);
else if(p.size()==siz){
if(x>*p.begin()){
p.erase(p.begin());
p.insert(x);
}
}
}
void pop(){
ans+=*prev(p.end());
p.erase(prev(p.end()));
}
signed main(){
n=read();m=read();k=read();
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
ar[j*n+i]=read();
}
}
top=0;int pos=-1;
for(int i=0;i<m;i++){
if(i>=k) --siz,pop();
if(i+k-1<m) ++siz;
for(int j=0;j<n;j++){
add(ar[++pos]);
}
}
while(siz--) pop();
printf("%lld\n",ans);
}
/*
3 6 3
1 2 9 1 2 6
1 2 9 1 2 6
1 2 9 1 2 6
*/
写在最后
本来有一道我最喜欢的 hina 大可爱的我最喜欢的字符串的题,但是题太多了删了,下届校赛再见嘻嘻。