CF 2004 Educational Codeforces Round 169 (Rated for Div. 2)
E - Not a Nim Problem
发现就是标准 Nim 游戏,
然后考虑如何求 SG 函数。
观察发现有:
然后这里先说结论,在说证明(赛时通过打表找规律)
对于一个数字 \(i\),如果 \(i\) 是偶数,那么 \(g_i=0\),如果 \(i\) 是奇合数,假设它的最小质因子为 \(p\),那么有 \(g_i=g_p\),如果 \(i\) 是奇质数,那么 \(g_i=id\),\(id\) 表示它是除了 \(2\) 以外的第几个质数。
证明:
首先对于 \(i\le 2\) 成立。
考虑 \(i+1\)
- \(i+1\) 是质数,其是第 \(id\) 个质数:
\([1,i]\) 就是它的后继状态,那么显然 \(g_2=0\),然后 \([1,id-1]\) 一定在前面出现过。所以 \(g_{i+1}=id\)。 - \(i+1\) 是偶数:
显然对于一个数 \(c\),如果 \(\gcd(i,c)=1\) 那么 \(c\) 一定是奇数,\(i+1-c\) 就一定是奇数,\([1,i]\) 中的奇数都不为 \(0\),所以 \(g_{i+1}=0\)。 - \(i+1\) 是奇合数:
其某个质因子是 \(p\),那么显然 \(p\) 的倍数一定不能减去,并且 \([1,i]\) 中满足 \(g_j=g_p\) 的一定有 \(p\mid j\),所以 \(g_{i+1}<g_p\),然后 \(g_p\) 最小的那个就是最小质因子的 SG。
证毕。
然后直接线性筛法即可。
时间复杂度:\(O(n+V)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for (int i = (l); i <= (r); i++)
#define per(i,r,l) for (int i = (r); i >= (l); i--)
#define pb push_back
#define eb emplace_back
#define mp make_pair
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
#define maxn(a, b) a = max(a, b)
#define minn(a, b) a = min(a, b)
typedef vector<int> VI;
typedef long long ll;
typedef pair<int,int> PII;
typedef double db;
const ll mod = 998244353;
mt19937 gen(114514);
ll rp(ll a,ll b) {ll res=1%mod;a%=mod; assert(b>=0); for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}
ll gcd(ll a,ll b) { return b?gcd(b,a%b):a;}
const int N = 1e7 + 1;
int sg[N];
bool b[N];
void preinit() {
int idx = 0;
sg[1] = ++idx;
VI p;
rep(i,2,N-1) {
if (!b[i]) {
p.pb(i);
if (i!=2) sg[i]=++idx;
}
for(auto x:p){
if (x*1ll*i>=N)break;
b[x*i]=true;
sg[x*i]=sg[x];
if (i%x==0) break;
}
}
}
void solve() {
// init();
int n, res = 0;
scanf("%d", &n);
rep(i,1,n) {
int a;
scanf("%d", &a);
res ^= sg[a];
}
puts(res ? "Alice" : "Bob");
}
int main() {
preinit();
int T;
scanf("%d", &T);
while (T--)
solve();
return 0;
}
F. Make a Palindrome
首先考虑对于一个序列直接怎么 \(O(n)\) 求 \(f\) 值,就发现考虑维护两个指针 \(l,r\),如果 \(a_l=a_r\),则 \(l+1,r-1\),否则我们就让小的那一个分裂,那么每次操作一定可以减少长度,所以最优。
然后就可以 \(O(n^3)\),考虑换一种可优化的方式计算 \(f\),通过猜想大概就是看一下前缀后缀集合有多少个不相等的数字。
发现能够过拍。
考虑优化,我们考虑枚举区间,区间的和为 \(val\),且这个区间作为某个前缀,其左端点为 \(l\),右端点为 \(r\)。
问题就是找有多少个 \(j(j\ge l)\) 满足:
\(\forall i(l\le i)~ s_{i,j} \ne val\)
于是我们考虑枚举了 \(l,r\) 之后看看有多少个 \(j\) 不满足条件即可。
然后发现某一个 \(j\) 所对应的 \(i\) 也一定只有至多一个满足 \(s_{i,j}=val\)。
问题就是问有多少个区间 \([i,j]\) 满足和 \(s_{i,j}=s_{l,r},(l\le i,r\le j)\)。
于是我们用 map
维护一个 int, vector<pair<int, int>>
表示值为 \(val\) 对应的区间。
然后按照左端点排序,对于每个区间看看它后面有多少个右端点大于它的右端点即可。
用树状数组维护。
时间复杂度:\(O(n^2\log n)\)。
Code:
#include <bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for (int i = (l); i <= (r); i++)
#define per(i,r,l) for (int i = (r); i >= (l); i--)
#define pb push_back
#define eb emplace_back
#define mp make_pair
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
#define maxn(a, b) a = max(a, b)
#define minn(a, b) a = min(a, b)
typedef vector<int> VI;
typedef long long ll;
typedef pair<int,int> PII;
typedef double db;
const ll mod = 998244353;
mt19937 gen(114514);
ll rp(ll a,ll b) {ll res=1%mod;a%=mod; assert(b>=0); for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}
ll gcd(ll a,ll b) { return b?gcd(b,a%b):a;}
template<class T>
struct BIT {
vector<T> bit;
int size;
void init(int n) {
bit.clear(); bit.resize(n + 1, 0);
size = n;
}
void update(int x, T v) {
assert(x);
while (x <= size) {
bit[x] += v;
x += (x & -x);
}
}
T query(int x) {
assert(x <= size);
T res = 0;
while (x) {
res += bit[x];
x -= (x & -x);
}
return res;
}
int find(T k) {
assert(k); k--;
int x = 0;
for (int i = log2(size); i >= 0; i--) {
int y = x + (1 << i);
if (y <= size && bit[y] <= k) {
x = y;
k -= bit[y];
}
}
return x + 1;
}
};
BIT<int> t;
const int N = 2043;
int n;
int a[N];
map<int, vector<PII>> p;
void solve() {
// init();
scanf("%d", &n);
rep(i,1,n) scanf("%d", &a[i]);
ll ans = 0;
p.clear();
per(i,n,1) {
int sum = 0;
rep(j,i,n) {
sum += a[j];
p[sum].eb(i, j);
}
}
for (auto [val, ve] : p) {
t.init(n);
// cout << "val: " << val << endl;
for (auto [l, r] : ve) {
ans += n - r - t.query(n - r + 1);
// cout << l << ' ' << r << endl;
t.update(n - r + 1, 1);
}
// cout << ans << endl;
}
printf("%lld\n", ans);
}
int main() {
int T;
scanf("%d", &T);
while (T--)
solve();
return 0;
}
update:
发现有更加简单的做法,仔细考虑区间的性质,发现一定不会有包含关系,所以随便乱搞计数即可:
Code:
#include <bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for (int i = (l); i <= (r); i++)
#define per(i,r,l) for (int i = (r); i >= (l); i--)
#define pb push_back
#define eb emplace_back
#define mp make_pair
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
#define maxn(a, b) a = max(a, b)
#define minn(a, b) a = min(a, b)
typedef vector<int> VI;
typedef long long ll;
typedef pair<int,int> PII;
typedef double db;
const ll mod = 998244353;
mt19937 gen(114514);
ll rp(ll a,ll b) {ll res=1%mod;a%=mod; assert(b>=0); for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}
ll gcd(ll a,ll b) { return b?gcd(b,a%b):a;}
const int N = 2043;
int n;
int a[N];
map<int, vector<PII>> p;
void solve() {
// init();
scanf("%d", &n);
rep(i,1,n) scanf("%d", &a[i]);
p.clear();
per(i,n,1) {
int sum = 0;
rep(j,i,n) {
sum += a[j];
p[sum].eb(i, j);
}
}
ll ans = 0;
rep(i,1,n) ans += i * (n - i + 1);
for (auto [val, ve] : p) {
int sz = SZ(ve);
ans -= sz * 1ll * (sz + 1) / 2;
}
printf("%lld\n", ans);
}
int main() {
int T;
scanf("%d", &T);
while (T--)
solve();
return 0;
}
G. Substring Compression
首先容易证明第偶数个数字一定是一位数。
证明:
假设这一对相邻的元素为 \(x,y\),那么对应的两种方案就是:
\(len\times x\),\((len-1)\times(10y+x)\)。
然后这里有 \(len>1\)。
于是我们作一下差:
\(\Delta = (len-x)\times(10y+x)-len\times x = 10y(len-1)-x\)
显然 \(len-1>0\),然后 \(10y>x\)。
所以 \(len\times x\) 长度更小,更优,可以直接替换。
然后我们得到一个 \(O(n^2)\) 算一个序列的 \(f\) 值的方法:
\(dp_i\) 表示上一个选择的长度在 \(i\) 这个位置。
然后:
考虑将式子拆一下:
于是我们把 \(s_i\) 记入状态中:
\(f_{i,c}\) 表示目前选的翻倍长度为 \(c\),然后考虑到了第 \(i\) 位的最小长度。
然后需要支持端点插入删除,求区间最小值。
EDU 题解里面有一个逆天做法。感觉非常不会,题解链接
但是不会没有关系,发现这篇题解做法非常的好。
我们考虑预处理出每个 \(k-1\) 的块的前缀后缀积,那么对于任意长度为 \(k\) 的连续段的矩阵积一定可以通过两端的后缀前缀拼起来。
然后还有一个小问题,矩阵只能 \(i\to i+1\) 的转移,那么对于第二个向 \(i+2\) 转移的式子怎么处理呢?
我们考虑新开一个状态:\(f_{i,0}\) 表示目前考虑前 \(i\) 个数字,从 \(i+1\) 开始强制重新开启新的一段的最小值。那么有:
这样就解决了。
时间复杂度:\(O(n\times10^3)\)。
Code:
#include <bits/stdc++.h>
using namespace std;
const int inf = 1e9;
const int N = 200010;
struct mat {
array<array<int, 10>, 10> v;
mat(int val = inf) {
for (int i = 0; i < 10; i++)
v[i].fill(val);
}
} a[N], pre[N], suf[N];
int n, k;
char t[N];
mat mul(const mat &a, const mat &b) {
mat c;
for (int i = 0; i < 10; i++) {
for (int k = 0; k < 10; k++) {
for (int j = 0; j < 10; j++) {
c.v[i][j] = min(c.v[i][j], a.v[i][k] + b.v[k][j]);
c.v[i][j] = min(c.v[i][j], a.v[i][k] + b.v[k][j]);
}
}
}
return c;
}
int main() {
scanf("%d%d", &n, &k);
scanf("%s", t + 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j < 10; j++) {
a[i].v[j][j] = a[i].v[j][0] = j;
}
a[i].v[0][t[i] - '0'] = 0;
}
for (int i = 1; i <= n; i++) {
pre[i] = a[i];
if (i % k != 1) {
pre[i] = mul(pre[i - 1], pre[i]);
}
}
for (int i = n; i >= 1; i--) {
suf[i] = a[i];
if (i % k && i < n) {
suf[i] = mul(suf[i], suf[i + 1]);
}
}
for (int i = k; i <= n; i++) {
mat f;
f.v[0][0] = 0;
f = mul(f, suf[i - k + 1]);
if (i % k) f = mul(f, pre[i]);
printf("%d ", f.v[0][0]);
}
puts("");
return 0;
}
本文作者:weirdoX
本文链接:https://www.cnblogs.com/weirdoX/p/18367049
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步