多校联测第四场复盘总结
本场比赛心态爆炸,寄完了,但是发现了不少问题
时间安排
8:00 - 8:10:看完T1, 数学题,感觉可以搞(这场比赛爆炸的开始)
8:10 - 8:30: 打完暴力,开始思考,首先每个数的
f
(
x
)
f(x)
f(x)不会超过50(关键性质)
8:30 - 9:00: 考虑每种数出现次数 * val, 但是好像需要容斥一下,然后就卡了
9:00 - 9:20: 尝试打表找规律
9:20 - 9:40 : 貌似找到了规律? 冲!
9:40 - 10:00 : 小样例过了? 大样例错了,寄,没时间了
10:00 - 10:30: 看T2, 发现专门给了一档只有行, 得到性质 只选行与顺序无关,只选列与顺序无关(关键性质),大胆猜测,行和列放一起也与顺序无关
10:30 - 10:50: T2码完,小样例过,大样例没过, 寄, 猜测打错了,查错…
11:00 - 11:20:T2调不出来,扔了
11:20 - 12:00:没时间了,寄, T3乱搞了随机化, T4乱搞了链和菊花,然后就寄完了
期望得分:60 + 50 + 20 + 20 = 150
实际得分: 60 + 50 + 0 + 0 = 110;
可拿的分: 100 + 100 + 40 + 50 = 290
订题情况
- T1
- T2
- T3
- T4
T1 奶牛的数学题
看题: 看到数据范围就应该想到这题是一道数学题,或者(矩阵乘法), 但显然无法写出线性递推式,所以应该往数学上思考
1.如果一个数的
f
(
x
)
=
i
f(x) = i
f(x)=i, 则
x
x
x最小为
l
c
m
(
1
i
)
lcm(1~i)
lcm(1 i), 可得
f
(
x
)
<
=
50
f(x) <= 50
f(x)<=50
2.将
n
u
m
[
i
]
∗
i
=
∑
i
=
1
i
<
=
50
∑
j
=
i
j
<
=
50
n
u
m
[
j
]
num[i] * i = \sum _{i = 1}^{i<=50}\sum_{j=i}^{j<=50} num[j]
num[i]∗i=∑i=1i<=50∑j=ij<=50num[j]
3. 所以题目转化为了对于每一个
i
∈
50
i \in50
i∈50
f
(
x
)
≥
i
f(x) \ge i
f(x)≥i的个数, 如何计算个数,通过1可得,若
f
(
x
)
≥
i
f(x) \ge i
f(x)≥i则
x
x
x必须为
l
c
m
{
1
,
2
,
3...
(
i
−
1
)
}
lcm\left \{ 1,2,3...(i-1) \right \}
lcm{1,2,3...(i−1)}的倍数,计算即可
#include<bits/stdc++.h> //将贡献拆为单位贡献
using namespace std;
#define LL long long
const int MAX = 1e4 + 70;
const int MOD = 1e9 + 7;
int main() {
freopen("math.in","r",stdin);
freopen("math.out","w",stdout);
int t; scanf("%d", &t);
while(t, t--) {
LL n; scanf("%lld", &n);
LL SUM = 1, ans = n % MOD;
for(int i = 2; i <= 50; i++) {
if(SUM > n) break;
ans = (ans + (n / SUM)) % MOD;
SUM = SUM * i / __gcd(SUM, (LL)i);
}
cout<<ans<<endl;
}
return 0;
}
T2路遇矩阵
![在这里插入图片描述](https://img-blog.csdnimg.cn/ebf8c6c09c734061994736bda3de3ef3.png
1.20pts的部分分是一个很好的切入点,我们发现只有行和只有列顺序无关,贪心的选一定最优
2.但是行和列放在一起,肯定不能贪心,因为如果选的次数
k
k
k非常大,行数小,列数恰好为
k
k
k,那么选
k
k
k列反而最优
如何解决? 枚举选多少个行,多少个列即可
//1. 删除顺序与答案统计无关,答案只与选择有关
//2. 只删行与只删列满足贪心性质
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 1e3 + 70;
int n, m, k, p;
int a[MAX][MAX];
LL ans = 0, ANS = -1e18, H[1100000], L[1100000];
multiset<LL> h, l;
int main() {
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
scanf("%d%d%d%d", &n, &m, &k, &p);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for(int i = 1; i <= n; i++) {
LL sum = 0;
for(int j = 1; j <= m; j++) sum += a[i][j];
h.insert(sum);
}
for(int j = 1; j <= m; j++) {
LL sum = 0;
for(int i = 1; i <= n; i++) sum += a[i][j];
l.insert(sum);
}
LL sum = 0;
for(int i = 0; i <= k; i++) { //控制删i个行
H[i] = sum;
sum += *h.rbegin();
LL NOW = *h.rbegin();
set<LL>::iterator it = h.end();
it--;
h.erase(it);
h.insert(NOW - (LL)p * m);
}
sum = 0;
for(int i = 0; i <= k; i++) {
L[i] = sum;
sum += *l.rbegin();
LL NOW = *l.rbegin();
set<LL>::iterator it = l.end();
it--;
l.erase(it);
l.insert(NOW - (1LL * p * n));
}
for(int i = 0; i <= k; i++) {
ANS = max(ANS, L[i] + (H[k - i] - (1LL * p * i * (k - i))));
}
cout<<ANS<<endl;
return 0;
}
T3:奶牛的括号匹配
一眼状压,但是如何设计状态呢?
首先套路的设定
f
(
i
)
f(i)
f(i)表示选定集合
i
i
i所能产生的最大前缀匹配个数
但是若将 j j j加入集合 i i i必须满足当前的 i i i集合的最大方案是可以拓展的
那我们规定 f ( i ) f(i) f(i)表示 i i i 集合的最大答案,且当前的排列顺序保证可以拓展 即 ( ( ( 的个数 始终 ≥ \ge ≥ ) ) )
那么如何计算 j j j对于集合 i i i的贡献,如果集合 i i i的剩余 ( {\color{Green} {\LARGE (} } ( 括号的个数为 k k k, 那么 j j j所能贡献的数量即为 c n t j k cnt_jk cntjk表示第 j j j个串前缀 ) {\color{Green} {\LARGE )} } )个数为 k k k的数量位置
//状压DP, 状态设计
// f[i] 表示集合i最大答案, 转移
// 往i中添加新的字符串j, 考虑j的贡献
// 若集合i匹配完仍剩OP个( 计算j的贡献
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 22;
const int oo = 114514123;
int n, ans, cnt[MAX][410000]; //表示第i个串,出现j的个数
int f[(1 << MAX)];
int maxx[MAX], END[410000]; //表示第j个的最大的) 和 每个串最后的值
string s[MAX];
int num[(1 << MAX)]; //表示集合i的剩余(个数
void prework(int id, string S) {
int sum = 0;
maxx[id] = -oo;
for(int i = 0; i < S.size(); i++) {
if(S[i] == '(') sum--;
else sum++;
maxx[id] = max(maxx[id], sum);
if(sum >= maxx[id]) cnt[id][sum]++;
}
END[id] = sum;
}
int main() {
freopen("seq.in","r",stdin);
freopen("seq.out","w",stdout);
scanf("%d", &n);
for(int i = 0; i < n; i++) cin>>s[i]; // 第i个串
for(int i = 0; i < n; i++) prework(i, s[i]); //预处理第i个串的信息
for(int i = 0; i <= (1 << 21) - 1; i++) f[i] = -oo;
f[0] = 0;
for(int i = 0; i <= (1 << n) - 1; i++) { //枚举 i集合
for(int j = 0; j < n; j++) { //选择第j个填加
if( (i >> j ) & 1) continue;
if(num[i] >= maxx[j]) { //说明可以更新下一个f集合
f[i | (1 << j)] = max(f[i | (1 << j)], f[i] + cnt[j][num[i]]);
num[i | (1 << j)] = num[i] - END[j];
ans = max(ans, f[i | (1 << j)]);
} else { //不可以更新f集合但是可以统计答案
ans = max(ans , f[i] + cnt[j][num[i]]);
num[i | (1 << j)] = num[i] - END[j];
}
}
}
cout<<ans<<endl;
return 0;
}
T4: 润不掉了
一步一步分析
20pts:爆搜,但是不好打
40pts:我们分析,如果对于当前的根为 r o o t root root
对于 r o o t root root的若干子树,什么子树需要贡献1(被一个点看守)的答案呢?
那么应该是子树 i i i中的叶节点到 r o i ro_i roi的最小距离 ≤ \le ≤ d i s ( x , r o i ) dis(x,roi) dis(x,roi),
我们预处理叶子节点到其他点的最小距离,枚举每一个点为根,向下递归答案即可
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 7e4 + 70;
int n, tot, head[MAX], du[MAX], len[MAX];
vector<int> son[MAX]; //
queue<int> q;
void BFS() {
while(!q.empty()) {
int now = q.front(); q.pop();
for(auto y : son[now]) {
if(len[y] > len[now] + 1) {
len[y] = len[now] + 1;
q.push(y);
}
}
}
}
int dfs(int now, int fa, int dis) {
if(len[now] <= dis) return 1;
if(du[now] == 1) return 1;
int sum = 0;
for(auto y : son[now]) {
if(y == fa) continue;
sum = sum + dfs(y, now, dis + 1);
}
return sum;
}
int main() {
freopen("run.in","r",stdin);
freopen("run.out","w",stdout);
scanf("%d", &n);
for(int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
son[u].push_back(v);
son[v].push_back(u); //存边
du[u] += 1; du[v] += 1;
}
memset(len, 0x3f, sizeof(len));
for(int i = 1; i <= n; i++) if(du[i] == 1) len[i] = 0, q.push(i);
BFS() ; // 处理
for(int i = 1; i <= n; i++) {
int ans = dfs(i, 0, 0) ; // 第i 个点向子树跑
printf("%d\n", ans);
}
return 0;
}
我们想,40pts没有拓展性,因为无论怎样,枚举根的操作已经限制了整个算法,如何拓展呢?
我们想对于 x x x有贡献子树的每个点都满足 g ( i ) ≤ d i s ( i , x ) ) g(i) \le dis(i,x)) g(i)≤dis(i,x)),
如果我们将整棵子树的贡献设为1的话,那么就是计算点对问题,显然淀粉质就可以了
下面思考树的子树的性质
∑
d
u
=
2
s
i
z
−
1
\sum{du}=2siz-1
∑du=2siz−1
变形
∑
d
u
−
2
s
i
z
=
1
\sum{du}-2siz=1
∑du−2siz=1
∑
d
u
−
2
=
1
\sum{du-2}=1
∑du−2=1
所以我们将每个点的val设为
d
u
−
2
du -2
du−2对于点
x
x
x的ans即为满足
g
(
i
)
≤
d
i
s
(
i
,
x
)
g(i)\le dis(i,x)
g(i)≤dis(i,x)的所有点的val之和
若
r
o
ro
ro为分治重心,统计过
r
o
ro
ro的点的答案
则
g
(
i
)
≤
d
i
s
(
r
o
,
i
)
+
d
i
s
(
r
o
,
x
)
g(i)\le dis(ro,i)+dis(ro,x)
g(i)≤dis(ro,i)+dis(ro,x)
则
g
(
i
)
−
d
i
s
(
r
o
,
i
)
<
=
d
i
s
(
r
o
,
x
)
g(i)-dis(ro,i)<=dis(ro,x)
g(i)−dis(ro,i)<=dis(ro,x)
树状数组维护
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 7e4 + 70;
int n, tot, head[MAX], du[MAX], len[MAX], val[MAX];
bool v[MAX];
vector<int> son[MAX]; //
queue<int> q;
int ro, maxx_tr, f[MAX], dis[MAX], siz[MAX], g[MAX], NUM;
int tree[MAX << 1], ans[MAX];
int lowbit(int x) { return x & (-x); }
void add(int x, int val) { for(int i = x; i <= 2 * n; i += lowbit(i)) tree[i] += val; }
int Find(int x) { int sum = 0; for(int i = x; i; i -= lowbit(i)) sum += tree[i]; return sum; }
void get_root(int x, int fa) { //求重心
siz[x] = 1, f[x] = 0;
for(auto To : son[x]) {
if(To == fa || v[To]) continue;
get_root(To, x);
siz[x] += siz[To];
f[x] = max(f[x], siz[To]);
}
f[x] = max(f[x], NUM - siz[x]);
if(f[x] < maxx_tr) {
maxx_tr = f[x];
ro = x;
}
}
void Clear() { maxx_tr = n + 1; }
void get_size(int x, int fa) {
siz[x] = 1;
for(auto y : son[x]) {
if(y == fa || v[y]) continue;
get_size(y, x);
siz[x] += siz[y];
}
}
void calc(int x, int fa) {
dis[x] = dis[fa] + 1;
ans[x] = ans[x] + Find(dis[x] + n); //加上n的偏移量
for(auto y : son[x]) {
if(y == fa || v[y]) continue;
calc(y, x);
}
}
void change(int x, int fa, int zf) {
add(n + g[x] - dis[x], zf * val[x]);
for(auto y : son[x]) {
if(y == fa || v[y]) continue;
change(y, x, zf);
}
}
void work(int x, int id) {
dis[x] = 0;
if(id == 1) add(g[x] + n, val[x]); //端点有x,x的影响
for(auto y : son[x]) {
if(v[y]) continue;
calc(y, x); //统计当前这颗子树的答案
change(y, x, 1); //将影响加上去
}
if(id == 1) ans[x] += Find(n);//计算端点值
for(auto y : son[x]) { //倒着做一次
if(v[y]) continue;
change(y, x, -1);
}
if(id == 1) add(g[x] + n, -val[x]);
}
void slove(int x) { //计算过x的点对之间的答案
v[x] = 1;
work(x, 1);
reverse(son[x].begin(), son[x].end());
work(x, 2);
for(auto y :son[x]) {
if(v[y]) continue;
Clear();
get_size(y, x);
NUM = siz[y];
get_root(y, x); //分治下去
slove(ro);
}
}
void BFS() {
while(!q.empty()) {
int now = q.front(); q.pop();
for(auto y : son[now]) {
if(g[y] > g[now] + 1) {
g[y] = g[now] + 1;
q.push(y);
}
}
}
}
int main() {
// freopen("run.in","r",stdin);
// freopen("run.out","w",stdout);
scanf("%d", &n);
for(int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
son[u].push_back(v);
son[v].push_back(u);
du[u] += 1; du[v] += 1;
}
memset(g, 0x3f, sizeof(g));
for(int i = 1; i <= n; i++) if(du[i] == 1) q.push(i), g[i] = 0; //多源最短路
BFS();
for(int i = 1; i <= n; i++) val[i] = 2 - du[i]; //问题转化,
Clear();
get_size(1, 0);
NUM = siz[1];
get_root(1, 0);
slove(ro);
for(int i = 1; i <= n; i++) {
if(du[i] == 1) printf("1\n");
else printf("%d\n", ans[i]);
}
return 0;
}