总结
- 如果题目真的又小又水,可以写硬编码,但是给它优化还是算了
- 像B这样的小编码分类题,可以按二进制编码,然后用按位或进行状态转移(还想做这样的题)
- 分类题一定要稳,尽量整合状态但是不要影响可读性,不要留没有用的变量。
- 当计数题没有思路时,可以先把算式列出来,再想办法优化。
- 学到了离散化的写法,尽量缩小变量的作用域。
- 得分是欧几里得距离平方,这意味着x和y的变化对于得分的贡献是相互独立的。
- 树上题的常用解法是dfs,用子树的答案来组成父节点的答案。(需要训练)
- 渐进复杂度小的算法不一定比大的算法优,因为常数与实现的复杂性。
1042A. Benches 水
n个椅子上分别坐着人,现在来了若干人可以任意地坐,问最大和最小可能的最大人数是多少。
最大:人全坐在原有人数最大的椅子上
最小:首先填满人数不最大的椅子,mx*n-sum,如果仍有剩余,除以mx取上整。最后结果加上mx。
1042B. Vitamins 水/背包(?)
n瓶饮料,每瓶都有一个价格和包含的维生素(A,B,C,至少一种),问最少花多少钱可以喝到ABC三种维生素。
饮料只有7种,每种保留最小价格,再取组合(ABC,AB+C,AC+B,BC+A,AB+AC,AB+BC,AC+BC,A+B+C)。
int main(void)
{
int n = read();
map<string,int> mp;
mp["A"] = mp["B"] = mp["C"] = mp["AB"] = mp["AC"] = mp["BC"] = mp["ABC"] = M;
for(int i=0;i<n;i++)
{
int c;
string p;
cin >> c >> p;
sort(p.begin(), p.end());
mp[p] = min(mp[p],c);
}
int ans = INT_MAX;
ans = min(ans,mp["A"]+mp["B"]+mp["C"]);
ans = min(ans,mp["A"]+mp["BC"]);
ans = min(ans,mp["B"]+mp["AC"]);
ans = min(ans,mp["C"]+mp["AB"]);
ans = min(ans,mp["AB"]+mp["BC"]);
ans = min(ans,mp["AC"]+mp["BC"]);
ans = min(ans,mp["AB"]+mp["AC"]);
ans = min(ans,mp["ABC"]);
printf("%d\n",ans>400000?-1:ans );
return 0;
}
硬编码可以说是非常难受了,比赛中的写法更难受,因为我试图去给硬编码去优化。
看了一下别人的做法,这个题使用了类似背包的东西(太巨了)
让A到ABC分别对应1到7,且A,B,C的权值分别是1,2,4,这样就可以使用按位或来补足没有的维生素。
每读入一个字符串,就更新它所有的能更新的状态,注意初始时save[0] = 0。
int save[8];
int main(void)
{
int n = read();
fill(save+1,save+8,MOD);
while(n--)
{
int c,st = 0;
string str;
cin >> c >> str;
for(auto x:str) st |= 1<< (x-'A');
for(int i=0;i<8;i++) save[i|st] = min(save[i|st], save[i]+c);
}
printf("%d\n",save[7]==MOD?-1:save[7] );
return 0;
}
1042C. Array Product 分类讨论
给定n个数,有两种操作:1.选择两个数,把左边的数乘到右边的数上,扔掉左边的数。2.扔掉一个数。第二个操作最多做一次,且每扔掉一个数后其它数的索引不变(方便选择)。现在要做n-1次操作,使得剩下的唯一一个数最大,给出方案。
分析这两种操作,会发现实际上可以扔掉一部分数,选择另一部分数相乘使积最大。分别统计正数,0,负数的个数。当存在0或者负数个数为奇数时,把最大的负数和所有0乘起来扔掉。需要排除(全为0)及(n-1个0,1个负数)的情况。
int save[M];
int main(void)
{
int n = read(), cnt_zero=0, cnt_nega=0, max_nega_id = -1, max_nega = INT_MIN;
for(int i=1;i<=n;i++)
{
save[i] = read();
if(save[i]==0) cnt_zero++;
else if(save[i]<0)
{
cnt_nega++;
if(save[i]>max_nega)
{
max_nega = save[i];
max_nega_id = i;
}
}
}
if(cnt_zero==n ||
(cnt_zero==n-1 && cnt_nega==1) ||
(cnt_zero==0 && (cnt_nega&1)==0)
)
{
for(int i=1;i<n;i++)
printf("1 %d %d\n",i,i+1);
}
else
{
vector<int> die,live;
for(int i=1;i<=n;i++)
{
if(save[i]==0 || (cnt_nega&1 && i==max_nega_id))
die.push_back(i);
else
live.push_back(i);
}
for(int i=0;i<(int)die.size()-1;i++)
printf("1 %d %d\n",die[i],die[i+1] );
printf("2 %d\n",die.back() );
for(int i=0;i<(int)live.size()-1;i++)
printf("1 %d %d\n",live[i],live[i+1] );
}
return 0;
}
1042D. Petya and Array 计数,数据结构
给出长度为n(2e5)的一个数组(±1e9),以及一个t(±1e14),问数组有多少个子串和小于t。
子串和首先求前缀和数组
p
r
e
[
i
]
=
Σ
j
=
1
i
a
[
j
]
pre[i] = \Sigma_{j=1}^ia[j]
pre[i]=Σj=1ia[j],问题就转化为求
Σ
L
=
1
n
Σ
R
=
L
n
(
p
r
e
[
R
]
−
p
r
e
[
L
−
1
]
<
t
)
\Sigma_{L=1}^n\Sigma_{R=L}^n(pre[R]-pre[L-1]<t)
ΣL=1nΣR=Ln(pre[R]−pre[L−1]<t)
当L恒定时,问题就是
Σ
R
=
L
n
(
p
r
e
[
R
]
<
t
+
p
r
e
[
L
−
1
]
)
\Sigma_{R=L}^n(pre[R]<t+pre[L-1])
ΣR=Ln(pre[R]<t+pre[L−1]),
从后往前考虑的话,问题转化为维护一个支持两个操作的数据结构:1.插入一个元素,2.求小于某个给定值的元素个数。
因为所有值都是已知的,可以离散化后用树状数组,也可以直接使用平衡树做。
Treap做法:
整理了一下treap模板.
ll pre[M];
int main(void)
{
ll n = read(), t = read();
for(int i=1;i<=n;++i)
pre[i] = read() + pre[i-1];
ll ans = 0;
Treap<ll> tr(n);
for(int l=n;l;--l)
{
tr.insert(pre[l]);
ans += tr.lower_bound(t+pre[l-1])-1; //求小于t+pre[l-1]的元素个数
}
printf("%I64d\n",ans );
return 0;
}
树状数组做法
第一次正经的写离散化,将所有需要离散化的东西扔进map里,然后逐个赋值就好。
ll pre[M];
void discretization(map<ll,int> &mp)
{
int foo = 1;
for(auto &x : mp)
x.second = foo++;
}
int main(void)
{
ll n = read(), t = read();
map<ll,int> nums;
for(int i=1;i<=n;++i)
{
pre[i] = read() + pre[i-1];
nums[pre[i]] = 1;
nums[pre[i]+t] = 1;
}
nums[0] = nums[t] = 1;
discretization(nums);
BinIdTree bt(M);
ll ans = 0;
for(int l=n;l;--l)
{
bt.add(nums[pre[l]],1);
ans += bt.sum(nums[pre[l-1]+t]-1);
}
printf("%I64d\n",ans );
return 0;
}
1042E. Vasya and Magic Matrix 算式,dp
给定一个n*m的数字矩阵和起始位置,每次当前位置等可能地跳到一个数字更小的位置并获得等于这两个位置欧几里得距离平方的得分,求不能跳时的得分期望。
假如已经算出了起始位置可以跳到的所有位置的得分期望,那么起始位置的得分期望就等于这些位置得分期望的和再加上起始位置到这些位置的欧几里得距离平方和,设当前位置是
(
x
0
,
y
0
)
(x0,y0)
(x0,y0),需要跳到
(
x
i
,
y
i
)
其
中
1
<
=
i
<
=
k
(x_i,y_i) 其中1<=i<=k
(xi,yi)其中1<=i<=k,则
k
∗
F
0
=
Σ
i
=
1
k
(
F
i
+
(
x
i
−
x
0
)
2
+
(
y
i
−
y
0
)
2
)
=
k
∗
(
x
0
2
+
y
0
2
)
+
Σ
(
F
i
+
x
i
2
+
y
i
2
)
−
2
x
0
Σ
x
i
−
2
y
0
Σ
y
i
\begin{aligned} k*F_0&=\Sigma _{i=1}^k(F_i+(x_i-x_0)^2 +(y_i-y_0)^2)\\ &=k*(x_0^2+y_0^2)+\Sigma (F_i+x_i^2+y_i^2) - 2x_0\Sigma x_i - 2y_0\Sigma y_i \\ \end{aligned}
k∗F0=Σi=1k(Fi+(xi−x0)2+(yi−y0)2)=k∗(x02+y02)+Σ(Fi+xi2+yi2)−2x0Σxi−2y0Σyi
然后这道题就转化为了一个排序后dp计算的题。
我使用了结构体存储,也可以pair<int,pair<int,int>>,排序更方便。
int save[M][M];
struct Item
{
int value;
int x;
int y;
}item[M*M];
ll power(ll a, ll b)
{
ll ans = 1;
while(b)
{
if(b & 1) ans = ans * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return ans;
}
int inv(int a){
return power(a, MOD - 2);
}
int getneed()
{
int r = read(), c=read();
int ret = 1;
while(r!=item[ret].x || c!=item[ret].y )
ret++;
return ret;
}
int main(void)
{
int n = read(), m = read();
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
{
save[i][j] = read();
item[(i-1)*m + j]= (Item){save[i][j],i,j};
}
sort(item+1,item+n*m+1,[](const Item&a,const Item&b){
return a.value < b.value;
});
ll need = getneed(), ans;
ll sum0 = 0, sumx = 0, sumy = 0;
ll tmp0 = 0, tmpx = 0, tmpy = 0;
int lst = 0;
for(int i=1;i<=need;i++)
{
ll x = item[i].x, y = item[i].y;
ans = (((x*x+y*y)%MOD*lst + sum0 - 2*x*sumx - 2*y*sumy)%MOD+MOD)%MOD;
//printf("%d %I64d\n",i,ans );
ans = (ans * inv(lst)) % MOD;
tmp0 = (tmp0 + ans + x*x + y*y) % MOD;
tmpx = (tmpx + x) % MOD;
tmpy = (tmpy + y) % MOD;
if(item[i].value != item[i+1].value)
{
sum0 = (sum0 + tmp0) % MOD;
sumx = (sumx + tmpx) % MOD;
sumy = (sumy + tmpy) % MOD;
tmp0 = tmpx = tmpy = 0;
lst = i;
}
}
printf("%I64d\n",ans );
return 0;
}
1042F. Leaf Sets 贪心,DFS
给定一棵树,求最少将所有叶子分成几个集合,集合中的点两两距离不超过k。
考虑dfs,每个子树根会有若干个到叶子的距离,把这些距离排序后选择最大的两个之和+1使其不超过k,那么他们和之前的都可以放进一个集合,将其它的各自独立成一个集合,最后把小集合向上传递。
这题看题解勉强看懂了,可是还是不知道是怎么想出来的orz
实现:找任意一个非叶节点开始dfs,
vector<int> save[M];
int ans=1;
int n,k;
int dfs(int now, int fa = -1) //返回rt为根的最大可合并距离
{
if(save[now].size()==1)
return 0;
vector<int> cur; cur.reserve(save[now].size());
for(auto nxt : save[now]) if(nxt!=fa)
cur.push_back(dfs(nxt, now) + 1);
solve();
}
int main(void)
{
n = read(), k = read();
for(int i=1;i<n;i++)
{
int x = read(), y = read();
save[x].push_back(y);
save[y].push_back(x);
}
int root = 1;
while(save[root].size()<2) root++;
dfs(root);
printf("%d\n",ans );
return 0;
}
将所有子节点的返回值处理到cur中,此时cur表示当前根节点到所有子树内叶子的距离集合。solve的目标是找到cur中两两之和不超过k的最大的位置,返回这个位置,并给ans加上剩余的位置。
O(nlogn)的做法是先排序,然后遍历找到分界值。
sort(cur.begin(), cur.end());
ans += cur.size()-1;
int pos = 0;
for(;pos<(int)cur.size()-1;pos++)
if(cur[pos]+cur[pos+1]<=k)
ans--;
else
break;
return cur[pos];
O(n)的做法是遍历几次,找到不超过k/2的最大值L,大于L的最小值R,再分类判断一下。
int lvalue = -1; //不超过k/2的最大值
for(auto x : cur)
if(x*2<=k && (lvalue==-1 || x>lvalue))
lvalue = x;
if(lvalue == -1)
{
ans += cur.size()-1;
return *min_element(cur.begin(),cur.end());
}
int rvalue = -1; //恰好大于value的最小值
for(auto x : cur)
if(x>lvalue && (rvalue==-1 || x<rvalue))
rvalue = x;
for(auto x : cur)
if(x > lvalue)
ans++;
if(rvalue==-1 || lvalue + rvalue > k)
return lvalue;
ans--;
return rvalue;
实际上,O(n)并不比O(nlogn)快多少(1076ms,1091ms),并且实现起来要复杂一些。