Educational Codeforces Round 80 (Rated for Div. 2)
A - Deadline
题意:有一个程序,本身会跑d天,假如用x时间优化的话,则要跑 \(\lceil\frac{d}{x+1}\rceil\) 天,求n天内是否能跑出来。
题解:由基本不等式,大概会在根号的位置取到最小值,在这附近直接枚举。事实上:
\(x+\lceil\frac{d}{x+1}\rceil\)
\(= x+1-1+\lceil\frac{d}{x+1}\rceil\)
\(\geq x+1-1+\frac{d}{x+1}\)
\(\geq 2\sqrt{d}-1\)
后一个等号只有当 \((x+1)*(x+1)=d\) 才可以取到最小值。而前一个等号未必能取到,所以应该是取上整之后有可能会多1,不过这样实在是把这道题搞得太复杂了。
void test_case() {
ll n, d;
scanf("%lld%lld", &n, &d);
ll l = max(0ll, (ll)sqrt(1.0 * d) - 50ll);
ll r = max(n, (ll)sqrt(1.0 * d) + 50ll);
for(ll i = l; i <= r; ++i) {
if(i + (d + i) / (i + 1) <= n) {
puts("YES");
return;
}
}
puts("NO");
}
还有一种思路是枚举1e6内的x。和基本不等式的想法差不多,只是写起来更加暴力。
B - Yet Another Meme Problem
题意:给定n,m,求[1,n]内的a和[1,m]内的b,满足 \(ab+a+b=conc(a,b)\) 。
连接运算可以 \(conc(a,b)\) 视作 \(a\times 10^{len(b)}+b\) ,移项去掉b,a不为0直接除掉。
得到这个: \(b+1=10^{len(b)}\)
显然b就是若干个9。
所以就是求[1,m]里面有多少个全9的数字,然后乘以n。
void test_case() {
ll a, b;
scanf("%lld%lld", &a, &b);
int cnt = 0;
ll cur = 9ll;
while(cur <= b) {
++cnt;
cur = cur * 10ll + 9ll;
}
printf("%lld\n", a * cnt);
}
C - Two Arrays
题意:一个长度不超过10的序列a,一个等长序列b,a是非降序,b是非升序,b的对应位置>=a,每个位置都可以填1~n的一个数字,求有多少种不同的方法。
题解:看出来很明显就只和最后一个位置有关,所以天然就可以dp,dp的时候很容易写出一个n^4的dp,不过想优化想了很久。最后把矩阵画出来就发现其实折线部分都是0所以可以直接搞一个二维前缀和求出左上角矩阵的和。
ll dp[12][1005][1005];
ll dpp[12][1005][1005];
void test_case() {
memset(dp, 0, sizeof(dp));
memset(dpp, 0, sizeof(dpp));
int n, m;
scanf("%d%d", &n, &m);
for(int j = 1; j <= n; ++j) {
for(int k = j; k <= n; ++k)
dp[1][j][k] = 1;
}
for(int j = 1; j <= n; ++j) {
for(int k = n; k >= j; --k) {
dpp[1][j][k] = dpp[1][j][k + 1] + dpp[1][j - 1][k]
+ dp[1][j][k] - dpp[1][j - 1][k + 1];
}
}
for(int i = 2; i <= m; ++i) {
for(int j = 1; j <= n; ++j) {
for(int k = j; k <= n; ++k) {
/*for(int pj = 1; pj <= j; ++pj) {
for(int pk = k; pk <= n; ++pk) {
if(pk < pj)
break;
dp[i][j][k] += dp[i - 1][pj][pk];
}
}
dp[i][j][k] %= MOD;*/
dp[i][j][k] = dpp[i - 1][j][k];
}
}
for(int j = 1; j <= n; ++j) {
for(int k = n; k >= j; --k) {
dpp[i][j][k] = (dpp[i][j][k + 1] + dpp[i][j - 1][k]
+ dp[i][j][k] - dpp[i][j - 1][k + 1]);
if(dpp[i][j][k] >= MOD)
dpp[i][j][k] %= MOD;
if(dpp[i][j][k] < 0)
dpp[i][j][k] = (dp[i][j][k] % MOD + MOD);
if(dpp[i][j][k] >= 0)
dpp[i][j][k] += MOD;
}
}
}
ll sum = 0;
for(int j = 1; j <= n; ++j) {
for(int k = j; k <= n; ++k)
sum += dp[m][j][k];
sum %= MOD;
}
sum = (sum % MOD + MOD) % MOD;
printf("%lld\n", sum);
}
当然也有组合的选法,注意到这个序列像是下面一样的:
1 1 2 2 3
8 8 7 4 4
把b序列旋转过来:
1 1 2 2 3 4 4 7 8 8
上面的例子, \(m=5\) ,假设 \(n=8\) ,为了方便直接记 \(m=10\) 。
问题:在m个位置中安排m个非降序的数字,每个数字的取值都是[1,n],求不同的方法数。
可以这样思考,类似上面的序列,加入一些“隔板”。
1 1 | 2 2 | 3 | 4 4 | | | 7 | 8 8
初始值为1,每经过一个“隔板”,数字就会上升1点,恰好有n-1个“隔板”,所以就是把这n-1个“隔板”安排在数字的格子之间(也可以在两边)。
错误示范:数字格子是有序的,隔板是无序的,先把隔板看成有序的,答案就是 \(A_{n-1+m}^{n-1+m}\) ,重复计数了隔板的顺序,除以 \(A_{n-1}^{n-1}\) 。
正确示范:其实数字格子也是无序的,因为它最终排在哪个位置才决定了它是几号格子。数字格子是无序的,隔板也是无序的,先都看成有序的,答案就是 \(A_{n-1+m}^{n-1+m}\) ,重复计数了隔板和数字格子的顺序,除以 \(A_{n-1}^{n-1}\) 再除以 \(A_{m}^{m}\) ,相当于 \(C_{n-1+m}^{m}\) 。所以直接就相当于在n-1+m个元素中恰好选n-1个变成隔板,其他的m个保留为数字。
const int MAXN = 1e6;
ll inv[MAXN + 5], fac[MAXN + 5], invfac[MAXN + 5];
void init_C(int n) {
inv[1] = 1;
for(int i = 2; i <= n; i++)
inv[i] = inv[MOD % i] * (MOD - MOD / i) % MOD;
fac[0] = 1, invfac[0] = 1;
for(int i = 1; i <= n; i++) {
fac[i] = fac[i - 1] * i % MOD;
invfac[i] = invfac[i - 1] * inv[i] % MOD;
}
}
inline ll C(ll n, ll m) {
if(n < m)
return 0;
return fac[n] * invfac[n - m] % MOD * invfac[m] % MOD;
}
void test_case() {
int n, m;
scanf("%d%d", &n, &m);
m *= 2;
init_C(n + m);
printf("%lld\n", C(n - 1 + m, m));
}
D - Minimax Problem
题意:一个n行m列的矩阵,找一对可能相同的i,j,使得这两行中,设bk为每列对应元素aik,ajk的最大值,使得b序列的的最小值最大。
题解:二分这个值,然后转化成验证其是否成立。
为了减少check的次数,先进行一次离散化,大概可以减少10次check(应该),优化了1/3常数。应该还可以找每行的最小值的最大值作为二分的L。
check里面,对每行计算一个可行向量,当两行的可行向量的或为全1时,则这两行为题目所要的解。
注意若一直check失败最后要补check一次L来真正更新答案,这里被hack了。因为这种二分是必定有解的所以忽略了最后check(L)的步骤,可能这种对低级题目的掌控全局的自满导致了我的FST吧,还好不是区域赛不然就惊喜30分钟了。以后二分到达边界之后把两个值都check一遍,确定一定会有解的话就把这种自满加在assert里面吧。
int a[300005][8];
int vis[1 << 8];
int b[2400005], btop;
int ansi, ansj;
int n, m;
bool check(int mid) {
memset(vis, 0, sizeof(vis));
for(int i = 1; i <= n; ++i) {
int cur = 0;
for(int j = 0; j < m; ++j)
cur = (cur << 1) | (a[i][j] >= mid);
vis[cur] = i;
}
int all1 = (1 << m) - 1;
for(int i = all1; i; --i) {
if(!vis[i])
continue;
for(int j = 0; j < m; ++j) {
if((i >> j) & 1)
vis[i ^ (1 << j)] = vis[i];
}
}
for(int i = 0; i < (1 << m); ++i) {
if(!vis[i])
continue;
int w = all1 ^ i;
if(vis[w]) {
ansi = vis[i];
ansj = vis[w];
return true;
}
}
return false;
}
void bs() {
int L = 1, R = btop;
while(true) {
int mid = (L + R) >> 1;
if(mid == L) {
if(check(R))
return;
else {
assert(check(L));
return;
}
}
if(check(mid))
L = mid;
else
R = mid - 1;
}
}
void test_case() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) {
for(int j = 0; j < m; ++j) {
read(a[i][j]);
b[++btop] = a[i][j];
}
}
sort(b + 1, b + 1 + btop);
btop = unique(b + 1, b + 1 + btop) - (b + 1);
for(int i = 1; i <= n; ++i) {
for(int j = 0; j < m; ++j)
a[i][j] = lower_bound(b + 1, b + 1 + btop, a[i][j]) - b;
}
bs();
printf("%d %d\n", ansi, ansj);
}
确实要注意n=1的情况?其实这个错误原因并不是n=1,就算n是别的也会错,是二分退出的时候虽然不需要验证,但是L不一定有被更新过。
E - Messenger Simulator
题意:一个n长序列,初始为1,2,3,...,n。然后接收一个m长的序列,每次把接收到的数字提到最前,询问每个数字移动到的最左和最右距离。
题解:一开始先初始化最左最右距离,发现把一个数字往前挪的时候会让后面的数字的当前距离+1,那么可以用线段树延迟。只有操作数字或者结束的时候下推标记并更新最右距离,被操作之后最左距离肯定是1。开个两倍长的线段树按序列的位置存放所有元素,用个数组记录每个数字的当前位置,然后操作这个数字的时候就把当前线段树的头到数字位置之间的位置全部+1。
不带优化的线段树:
404 ms
15300 KB
int Lmost[300005], Rmost[300005];
struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
static const int MAXN = 600000;
int a[MAXN + 5];
int lazy[(MAXN << 2) + 5];
void PushDown(int o, int l, int r) {
if(lazy[o]) {
lazy[ls] += lazy[o];
lazy[rs] += lazy[o];
int m = l + r >> 1;
lazy[o] = 0;
}
}
void Build(int o, int l, int r) {
if(l == r)
lazy[o] = a[l];
else {
int m = l + r >> 1;
Build(ls, l, m);
Build(rs, m + 1, r);
lazy[o] = 0;
}
}
void Update(int o, int l, int r, int ql, int qr, int v) {
if(ql <= l && r <= qr) {
lazy[o] += v;
return;
} else {
PushDown(o, l, r);
int m = l + r >> 1;
if(ql <= m)
Update(ls, l, m, ql, qr, v);
if(qr >= m + 1)
Update(rs, m + 1, r, ql, qr, v);
}
}
int Query(int o, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) {
return lazy[o];
} else {
PushDown(o, l, r);
int m = l + r >> 1;
int res = 0;
if(ql <= m)
res = res + Query(ls, l, m, ql, qr);
if(qr >= m + 1)
res = res + Query(rs, m + 1, r, ql, qr);
return res;
}
}
#undef ls
#undef rs
} st;
int pos[300005];
void test_case() {
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) {
pos[i] = i + m;
st.a[i + m] = i;
Lmost[i] = i;
Rmost[i] = i;
}
st.Build(1, 1, n + m);
// for(int j = 1; j <= n + m; ++j)
// printf("%d ", st.Query(1, 1, n + m, j, j));
// puts("");
int curp = m;
for(int i = 1; i <= m; ++i) {
int x;
scanf("%d", &x);
int p = curp--;
Rmost[x] = max(Rmost[x], st.Query(1, 1, n + m, pos[x], pos[x]));
Lmost[x] = 1;
st.Update(1, 1, n + m, p, pos[x], 1);
pos[x] = p;
// for(int j = 1; j <= n + m; ++j)
// printf("%d ", st.Query(1, 1, n + m, j, j));
// puts("");
}
for(int x = 1; x <= n; ++x) {
Rmost[x] = max(Rmost[x], st.Query(1, 1, n + m, pos[x], pos[x]));
printf("%d %d\n", Lmost[x], Rmost[x]);
}
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
//srand(time(0));
int t = 1;
//scanf("%d", &t);
for(int i = 1; i <= t; ++i) {
//printf("Case #%d:\n", i);
test_case();
}
}
应该也有种办法是用平衡树来做的,一样的,每个数字被操作之前和结束之后是最右位置的可能取值,这个可以在操作的时候利用rank来的维护,但是麻烦的是怎么知道每个元素在树上的什么位置。看起来比较方便的办法也是平衡树存个pair,然后每次生成一个新的头往前加。仔细想想不需要pair,根本不关心第二个关键字。
不带优化的平衡树(有旋Treap版本)
623 ms
17600 KB
int Lmost[300005], Rmost[300005];
#define ls ch[id][0]
#define rs ch[id][1]
const int MAXN = 600000 + 5;
int ch[MAXN][2], dat[MAXN];
int val[MAXN];
int cnt[MAXN];
int siz[MAXN];
int tot, root;
inline void Init() {
tot = 0;
root = 0;
}
inline int NewNode(int v, int num) {
int id = ++tot;
ls = rs = 0;
dat[id] = rand();
val[id] = v;
cnt[id] = num;
siz[id] = num;
return id;
}
inline void PushUp(int id) {
siz[id] = siz[ls] + siz[rs] + cnt[id];
}
inline void Rotate(int &id, int d) {
int temp = ch[id][d ^ 1];
ch[id][d ^ 1] = ch[temp][d];
ch[temp][d] = id;
id = temp;
PushUp(ch[id][d]);
PushUp(id);
}
//插入num个v
inline void Insert(int &id, int v, int num) {
if(!id)
id = NewNode(v, num);
else {
if(v == val[id])
cnt[id] += num;
else {
int d = val[id] > v ? 0 : 1;
Insert(ch[id][d], v, num);
if(dat[id] < dat[ch[id][d]])
Rotate(id, d ^ 1);
}
PushUp(id);
}
}
//删除至多num个v
void Remove(int &id, int v, int num) {
if(!id)
return;
else {
if(v == val[id]) {
if(cnt[id] > num) {
cnt[id] -= num;
PushUp(id);
} else if(ls || rs) {
if(!rs || dat[ls] > dat[rs])
Rotate(id, 1), Remove(rs, v, num);
else
Rotate(id, 0), Remove(ls, v, num);
PushUp(id);
} else
id = 0;
} else {
val[id] > v ? Remove(ls, v, num) : Remove(rs, v, num);
PushUp(id);
}
}
}
//查询v的排名,排名定义为<v的数的个数+1。
int GetRank(int id, int v) {
int res = 1;
while(id) {
if(val[id] > v)
id = ls;
else if(val[id] == v) {
res += siz[ls];
break;
} else {
res += siz[ls] + cnt[id];
id = rs;
}
}
return res;
}
int pos[300005];
void test_case() {
int n, m;
scanf("%d%d", &n, &m);
Init();
for(int i = 1; i <= n; ++i) {
pos[i] = i + m;
Lmost[i] = i;
Rmost[i] = i;
Insert(root, pos[i], 1);
}
// for(int j = 1; j <= n + m; ++j)
// printf("%d ", st.Query(1, 1, n + m, j, j));
// puts("");
int curp = m;
for(int i = 1; i <= m; ++i) {
int x;
scanf("%d", &x);
int p = curp--;
Rmost[x] = max(Rmost[x], GetRank(root, pos[x]));
Lmost[x] = 1;
Remove(root, pos[x], 1);
pos[x] = p;
Insert(root, pos[x], 1);
// for(int j = 1; j <= n + m; ++j)
// printf("%d ", st.Query(1, 1, n + m, j, j));
// puts("");
}
for(int x = 1; x <= n; ++x) {
Rmost[x] = max(Rmost[x], GetRank(root, pos[x]));
printf("%d %d\n", Lmost[x], Rmost[x]);
}
}
Nanako说可以用莫队,这怎么搞啊。还有双指针的,莫队是不是双指针的一种呢?