2023.7.7 NOIP模拟赛
赛时记录
A.发现没敢给大样例 应该是诈骗题 等会回来再看
B.首先想到区间\(DP\) 但是复杂度挂了
C.没看懂 再说吧 反正 \(40pts\) 应该是状压
D.怎么又是数论啊 难绷 可能能用 \(ST\) 表暴力骗点分
回来看A题吧 前30分的暴力分都很好敲 先不管
我们考虑 \(d = 3\) 的情况 一个很直觉的想法就是只要 \(d\) 不是 \(2\) 就都无解
但是这是不可能的 因为没有限制的数据有 70 分 要是固输 NO 有 70 分就太扯淡了 这题还没有多测
首先我们要明确一个问题 就是对于每一个骰子而言 它六面的数字应该是全部等价的 因为最上面的数字非常的随机
然后呢我们发现不可能让每个骰子每一位都是相同的 所以不能限制比如固定哪一位是 \(0\)
比如说\(3\) 我们可以构造它的倍数 \(6:110\) 或者是 \(12:1100\) 那我们的想法只能是保证后面几位的 前面因为不同数字间必然至少会有几位会有\(10\)的差异
那么它们在那几位上就不等价了 那就无解了
然后我们发现\(1110\)即\(14\)不是\(6\)的倍数 \(10110\)即\(22\)也不是\(6\)的倍数
那么我们现在需要找一个这样的数 我们保证后面几位 前面数字不管是啥都是它的倍数
然后我们发现\(4:100\) \(1100\)即\(12\)是它的倍数 \(1000\)即\(8\)是它的倍数 只要我们能保证最后两位是\(0\) 那么前面若干位就表示是\(4\)的几倍
那么我们盲猜只有\(d\)是\(2\)的倍数才有解 否则无解(也只能这么干了)
反正这么干那30应该是稳的 噢对需要特判一下\(n=1\)
但是疑点就在于数的取值范围 \(n\)的范围与 \(d\) 的范围 但我也没啥别的招了()
发现好像不对 只要选\(6\)个数出来 它们里面随便选任何个异或和都为\(d\)的倍数即可 \(6\)的枚举也就\(63\)种 可以爆搜 但是先不考虑写 比较耽误时间
upd:上面这玩意确实假了
看B题看能写出来多少 记得留大概1.5h去写T3的疑似状压和T4的\(ST\)表
首先这个查区间 \(mex\) 大不了上主席树 先不管 主要考虑怎么分
首先 \(n^2 logn\) 的暴力 \(DP\) 很好想 但是有着与其分值配不上的码量
\(c_i\) 怎么只有20啊/fn 要我我就出到 \(10^5\) 强制码主席树((
首先贪心是不可以的 比如 0 1 2 3 4 5 6 7 0 8 9 是不可能嗯贪的
问题还是部分分给的太少了啊啊啊啊啊啊啊啊
我们考虑每个元素对答案的贡献 当且仅当它是前面一段区间的 \(mex\) 时 选它才会让答案+1 否则对答案没有贡献
问题又来了 实际上不仅仅只是+1 测。
还剩两个小时 切不掉了 写部分分吧 **
01性质挂了 有时间记得回来修
T4的\(ST\)表写完了 写挂了。
啥也别说了 修吧。
破防了 怎么这么菜啊。
调不出来 心态炸了。
A题假做法 B题调不出来 C会写但不完全会写 D写炸了
不是为什么我小数和样例输出都能对上一到取模的就歇菜啊
我寻思我模数也妹抄错啊
不是为啥还要加捆绑测试啊
还剩15分钟 无所谓了已经
真考虑考虑要不滚回去学文化课吧
预计:30 + 20 + 0 + 0
实际:30 + 20 + 15 + 0 rk15
C题固输NO混了15分
考完 bot 告诉我 \(lcm\) 不能取模 所以啥也没写挂但是调不出来
好似。
我学到了什么
开玩笑 啥也没学到
知识层面:
- 有的题确实不会出现输出NO的情况
- 当某些数据范围很小可以考虑把它当常数想做法
- \(lcm\) 不能取模!!!!
- 因为对 \(x\) 进行质因数分解时 \(> \sqrt{x}\) 的质因数最多只会出现一次 所以可以考虑以 \(\sqrt x\) 为界分开处理
策略层面:
- 首先别老惦记着切题 把能打出来的暴力都打了再说 你要是没切掉暴力还没打出来不就完犊子了吗?你要是前面没切掉你还有心情打暴力?你要是没有暴力你想拍拿什么拍?
- 记得把每道题的暴力都预留好足够的时间 要是一道题实在修不出来就放了吧
补题
A.
首先问题可以简化成选六个不重复的数 让每个骰子上都是这六个数 使得从中选择任意个都是 \(d\) 的倍数
那我们很明显假如说弄一车的 \(0\) 就可以让它们异或起来毫无影响
但是要弄六个不同的 其中肯定有 \(0\) 和 \(d\)
我们考虑 因为 \(d\le 60\) 所以它的二进制最多有 \(6\) 位
那我如果在 \(7-12\) 位塞和 \(d\) 的 \(1-6\) 位一样的 那最后答案有没没有这个一定都是 \(d\) 的倍数 (因为有的话就相当于 \(d\) 的 \(64\) 位)
所以我们能构造出 \(d000000\) 和 \(dd\)
那么进一步 再塞到 \(13-18\) 位 然后就做完了
code:
#include <bits/stdc++.h>
using namespace std;
int n, d;
int main() {
scanf("%d%d", &n, &d);
printf("Yes\n");
for (int i = 1; i <= n; ++i) printf("%d %d %d %d %d %d\n", 0, d, 64 * d, 64 * d + d, 64 * 64 * d, 64 * 64 * d + d);
return 0;
}
B.
首先就像赛时说的 \(O(n ^ 2)\) 的 \(DP\) 很白给 直接设 \(f_i\) 表示 \(i\) 位置的最大美丽度
然后转移的时候枚举那个取 \(mex\) 的区间即可 可以暴力统计
code:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 0721;
int a[N], f[N], pre[N], ans[N], top, sum;
bool vis[31];
int n;
int find_mex(int l, int r) {
for (int i = 0; i <= 25; ++i) vis[i] = 0;
for (int i = l; i <= r; ++i) vis[a[i]] = 1;
for (int i = 0; i <= 25; ++i) {
if (!vis[i]) return i;
}
}
void solve1() {
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < i; ++j) {
int mex = find_mex(j + 1, i);
if (f[i] < f[j] + mex) {
pre[i] = j + 1;
f[i] = f[j] + mex;
// cout << i << " " << pre[i] << endl;
}
}
}
int tmp = n;
while (tmp != 0) {
// cout << tmp << " ";
ans[++top] = pre[tmp];
tmp = pre[tmp] - 1;
}
printf("%d %d\n", f[n], top);
for (int i = top; i >= 1; --i) printf("%d ", ans[i]);
}
void solve2() {
for (int i = 1; i <= n; ++i) {
if (a[i] == 0) {
if (vis[1]) sum += 2;
else {
++sum;
ans[++top] = i;
vis[1] = 0;
vis[0] = 1;
}
} else {
if (vis[1] == 0) {
vis[1] = 1;
if (vis[0]) ++sum;
}
else {
ans[++top] = i;
vis[1] = 1;
vis[0] = 0;
}
}
}
printf("%d %d\n", sum, top);
for (int i = 1; i <= top; ++i) printf("%d ",ans[i]);
}
int main() {
// freopen("cut3.in", "r", stdin);
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
if (n > 1e3) solve2();
else solve1();
return 0;
}
那我们考虑优化 首先 \(f_i\) 一定是单调不减的一段序列
并且我们发现 \(c_i \le 20\) 这说明 \(mex\) 一共就只有 \(22\) 种取值
我们可以维护出每个 \(i\) 能取到每种 \(mex\) 的最靠后的位置 然后枚举 \(22\) 种取值即可
这里选择建了一棵权值线段树 然后查找前缀所有数出现的最早时间
据说有一种用桶的 \(O(n)\) 的预处理
code:
#include <bits/stdc++.h>
#define ls (k << 1)
#define rs (k << 1 | 1)
#define mid (l + r >> 1)
using namespace std;
const int N = 5e5 + 0721;
int mex[N][25], f[N], pre[N], a[N];
int ans[N], top;
int n;
struct tree {
int minn[101];
void modify(int k, int l, int r, int loc, int v) {
if (l == r) {
minn[k] = v;
return;
}
if (loc <= mid) modify(ls, l, mid, loc, v);
else modify(rs, mid + 1, r, loc, v);
minn[k] = min(minn[ls], minn[rs]);
}
int query(int k, int l, int r, int u, int v) {
if (u <= l && v >= r) return minn[k];
int ret = 0x7fffffff;
if (u <= mid) ret = min(ret, query(ls, l, mid, u, v));
if (v > mid) ret = min(ret, query(rs, mid + 1, r, u, v));
return ret;
}
} seg;
void init() {
for (int i = 1; i <= n; ++i) {
seg.modify(1, 0, 21, a[i], i);
for (int j = 1; j <= 21; ++j) {
mex[i][j] = seg.query(1, 0, 21, 0, j - 1);
// cout << i << " " << j << " " << mex[i][j] << endl;
}
}
}
void dp() {
for (int i = 1; i <= n; ++i) pre[i] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= 21; ++j) {
if (mex[i][j] == 0) continue;
// cout << i << " " << j << " " << mex[i][j] << endl;
if (f[i] < f[mex[i][j] - 1] + j) {
pre[i] = mex[i][j];
f[i] = f[mex[i][j] - 1] + j;
// cout << i << " " << f[i] << " " << pre[i] << endl;
}
}
if (f[i - 1] > f[i]) {
pre[i] = pre[i - 1];
f[i] = f[i - 1];
}
}
}
void get_ans() {
int tmp = n;
// cout << pre[tmp] << endl;
while (tmp > 0) {
ans[++top] = pre[tmp];
tmp = pre[tmp] - 1;
// cout << tmp << endl;
if (tmp < 0) break;
}
printf("%d %d\n", f[n], top);
for (int i = top; i >= 1; --i) printf("%d ", ans[i]);
}
int main() {
// freopen("cut3.in", "r", stdin);
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
init();
dp();
get_ans();
return 0;
}
C.
实际上就是找若干个简单环 然后判这些环能不能恰好不重不漏地覆盖所有点
那实际上就是判能不能删一些边让每个点的出度入度都为 \(1\)
那我们把每个点的出点和入点分开 然后入点都连源点 出点都连汇点 每条边的最大流量都为 \(1\) 跑最大流 判这个最大流和 \(n\) 等不等即可
code:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e6 + 0721;
const ll inf = 0x7ffffffffffffff;
int dis[N], cur[N];
int head[N], nxt[N], to[N], val[N], cnt = 1;
int n, m, s, t;
ll ans;
vector<int> v[N];
int _nxt[N], tot;
bool vis[N];
inline void cmb(int x, int y, int z) {
to[++cnt] = y;
val[cnt] = z;
nxt[cnt] = head[x];
head[x] = cnt;
}
bool bfs() {
queue<int> q;
for (int i = s; i <= t; ++i) dis[i] = -1;
for (int i = s; i <= t; ++i) cur[i] = head[i];
dis[s] = 0;
q.push(s);
while (!q.empty()) {
int now = q.front();
q.pop();
if (now == t) return 1;
for (int i = head[now]; i; i = nxt[i]) {
int y = to[i];
if (dis[y] == -1 && val[i] > 0) {
dis[y] = dis[now] + 1;
q.push(y);
}
}
}
return 0;
}
int dinic(int x, ll res) {
if (x == t || res == 0) return res;
ll flow = 0, c;
for (int i = cur[x]; i != 0 && res; i = nxt[i]) {
cur[x] = i;
int y = to[i];
if (val[i] > 0 && dis[y] == dis[x] + 1) {
c = dinic(y, min(res, (ll)val[i]));
if (c == 0) dis[y] = -1;
val[i] -= c;
val[i ^ 1] += c;
flow += c;
res -= c;
}
}
return flow;
}
int main() {
// freopen("hamilton1.in", "r", stdin);
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int x, y;
scanf("%d%d", &x, &y);
cmb(x, y + n, 1);
cmb(y + n, x, 0);
}
s = 0, t = (n << 1) + 1;
for (int i = 1; i <= n; ++i) {
cmb(s, i, 1);
cmb(i, s, 0);
cmb(i + n, t, 1);
cmb(t, i + n, 0);
}
while (bfs()) ans += dinic(s, inf);
// cout << ans << endl;
if (ans == n) printf("YES\n");
else {
printf("NO\n");
return 0;
}
for (int i = 1; i <= n; ++i) {
for (int j = head[i]; j; j = nxt[j]) {
if (val[j] == 0) {
_nxt[i] = to[j];
break;
}
}
}
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
++tot;
int tmp = i;
while (!vis[tmp]) {
vis[tmp] = 1;
v[tot].push_back(tmp);
tmp = _nxt[tmp] - n;
// cout << tmp << endl;
}
}
}
for (int i = 1; i <= tot; ++i) {
printf("%d ",v[i].size());
for (int j = 0; j < v[i].size(); ++j) printf("%d ",v[i][j]);
}
return 0;
}
D.
一个很典但是我不知道的东西:对于每个数 \(x\) 而言 \(>\sqrt{x}\) 的质因子只会出现一次
所以我们将质因子分为 \(\le \sqrt{x}\) 和 \(>\sqrt{x}\) 的两类
因为我们实际上只需要查区间每个质因子出现次数的最大值
对于小质数 我们给每个小质数开一棵线段树维护其区间出现次数最大值
对于大质数 我们维护每个大质数的前缀和 然后作差判断这个区间内有没有出现即可 因为只要出现了次数一定是 \(1\)
这样就是 \(70pts\)
据说要切掉需要 莫队 + \(bitset\) + 分块一堆东西来优化求大质数那个