【题解】Educational Codeforces Round 145(CF1809)
A.Garland
题目描述:
有 \(4\) 只灯泡,第 \(i\) 只灯泡的颜色为 \(s_i\) 。
一开始,所有灯泡都是关着的,你需要把它们都打开。你可以进行数次操作,每次操作改变一个灯泡的状态,即打开原本关着的灯泡或关上原本亮着的灯泡。第一次操作可选择任何灯泡,此后每一次被操作的灯泡的颜色都不得与上次操作的灯泡颜色相同。现求最少需要的操作数,如果无法打开所有灯泡输出 \(-1\) 。
题目分析;
因为只有四只灯泡,所以我们可以直接分类讨论一下出现最多的颜色的个数,然后就做完了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
char s[100];
int cnt[100];
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%s",s+1);
int mx = 0;
for(int i=1; i<=4; i++){
cnt[s[i] - '0']++;
mx = max(mx,cnt[s[i] - '0']);
}
if(mx <= 2) printf("4\n");
else if(mx == 3) printf("6\n");
else if(mx == 4) printf("-1\n");
for(int i=0; i<=9; i++) cnt[i] = 0;
}
return 0;
}
B.Points on Plane
题目描述:
给你一个二维平面,你需要在其上放置 \(n\) 个棋子。
你只能将棋子放在整数坐标的点上。将棋子放在坐标点 $ (x, y) $ 上的成本等于 $ |x| + |y| $ (其中,\(|a|\) 表示 \(a\) 的绝对值)。
放置 \(n\) 个棋子的成本等于其中最大的单个棋子的成本。
你需要在平面上布置 \(n\) 个棋子,使得每对棋子之间的欧几里得距离严格大于 \(1\) ,且成本尽可能小。
题目分析:
我们一个显然的想法肯定就是从尽可能靠近原点的开始放,然后一层层向外放,使得每一个棋子之间满足距离严格大于 \(1\)。
但是要注意的一点就是,我们不一定要将棋子放在原点上,可能放在外围的一圈会更优。
这样就是考虑我们这样放出来的话棋子数是多少,如下图所示:
黄色方格构成的就是 \(d = 4\) 的最优放置方式,而黄色方格中间的红色方格就是 \(d = 3\) 的最优放置方式,那么这个数量是多少呢,我们可以将图旋转 \(45°\),这样其实就是对应一个正方形,也就是说 \(d = x\) 的最优放置方案下可以放置 \((x+1)^2\) 个棋子。
所以说对于 \(n\) 就找到最小的 \(x\) 使得 \((x+1)^2 \le n\) 即可,这个直接可以调用 sqrt
函数。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
int T;scanf("%lld",&T);
while(T--){
int x;scanf("%lld",&x);
int p = sqrt(x);
if(p * p > x) --p;
if(p * p < x) ++p;
printf("%lld\n",p-1);
}
return 0;
}
C.Sum on Subarrays
题目描述:
给定两个正整数 \(n\) 和 \(k\),构造一个长度为 \(n\) 的序列 \(a\) 满足:
- \(-1000\le a_i\le 1000\)
- 序列有恰好 \(k\) 个子串的和为正数
- 剩余 \(\frac{n(n+1)}2-k\) 个子串的和都是负数
你只需要输出任意一组解。
\(n \le 30\)
题目分析:
对于连续的 \(x\) 个正数,其会产生 \(\frac{x(x+1)}{2}\) 个符合条件的子串,因为 \(n \le 30\),所以直接在外侧放一个 \(-1000\) 就直接杜绝了其它的子串也是正数的情况了。
但是如果 \(k \not= \frac{x(x+1)}{2}\) 其实也是简单的,如果我们找到最大的 \(x\) 使得 \(k < \frac{x(x+1)}{2}\) 的话,那么我们缺少的符合条件的子串数量一定不超过 \(x\),也就是说我们可以通过在外侧放一个较小的负数,使得缺的补上就好了。
一个细节就是我们不能让子串和为 \(0\),所以我们在放正数的时候不能放 \(1\),这样会导致我们补的较小的负数为 \(0\)。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int a[1000];
int main(){
int T;scanf("%d",&T);
while(T--){
int n,k;scanf("%d%d",&n,&k);
int p = 0;
while(p * (p + 1) / 2 < k) p++;
if(p * (p + 1) / 2 == k){
for(int i=1; i<=p; i++) a[i] = 11;
for(int i=p+1; i<=n; i++) a[i] = -1000;
}
else{
--p;
int tmp = k - p * (p + 1) / 2;
tmp = p - tmp;
for(int i=1; i<=p; i++) a[i] = 11;
a[p+1] = -tmp * 11 - 1;
for(int i=p+2; i<=n; i++) a[i] = -1000;
}
for(int i=1; i<=n; i++) printf("%d ",a[i]);
printf("\n");
}
return 0;
}
D.Binary String Sorting
题目描述:
给定一个 \(01\) 字符串,你可以进行以下两种操作:
-
交换相邻的两个元素,此操作需要花费 \(10^{12}\) 个金币。
-
删除某个元素,此操作需要花费 \(10^{12}+1\) 个金币。
你要通过以上两种操作使这个字符串单调不降,求最小金币花费。
\(1 \le n \le 3\times 10^5\)
题目分析:
这种离谱的贡献其实就是让我们在最小化总操作次数的基础上,最小化交换的操作次数。
在逆序对的角度来看,交换操作相当不优,因为交换只能减少一个逆序对,而删除可以直接干掉很多个。
但是如果直接按逆序对个数判断是不是应该交换的话就很难受,不好实现,但是注意到我们是一个 \(01\) 字符串,也就是说我们排序之后一定是形如 \(0000 \cdots 000111 \cdots 1111\),也就是说我们可以直接枚举 \(01\) 的分界点,这样我们就是要求前面全部变成 \(0\) 后面全部变成 \(1\)。
稍微自己讨论一下就会发现除了在分界点处的 \(10\) 这种情况直接交换是优于删除的,其它的情况直接删除就是最优的。
所以就预处理出前缀的 \(1\) 和后缀的 \(0\) 的个数,然后特判一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF = 1e18;
const int base = 1e12 + 1;
const int N = 1e6+5;
char s[N];
int pre[N],suf[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
scanf("%s",s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++) pre[i] = pre[i-1] + (s[i] == '1');
suf[n+1] = 0;
for(int i=n; i>=1; i--) suf[i] = suf[i+1] + (s[i] == '0');
int ans = INF;
for(int i=0; i<=n; i++){
if(s[i] == '1' && s[i+1] == '0') ans = min(ans,(pre[i] + suf[i+1] - 2) * base + (base - 1));
else ans = min(ans,(pre[i] + suf[i+1]) * base);
}
printf("%lld\n",ans);
}
return 0;
}
E.Two Tanks
题目描述:
有两个水桶 A 和 B,容量分别为 \(a,b\) 升。
顺次进行 \(n\) 次操作,每次给定一个参数 \(v_i\),若 \(v_i\ge 0\),则从 A 中倒出 \(v_i\) 升水给 B,否则从 B 中倒出 \(|v_i|\) 升水给 A。
倒水过程中,若接收桶满或倒水桶空,则立即停止。
你需要对两桶水每种可行的初始水量 \((c,d)\) 求出,以该初始水量顺次进行这 \(n\) 次操作后,A 桶的水量。
\(n\le 10^4\),\(a,b,|v|\le 1000\)。
题目分析:
这 \(n\) 次操作看上去就没什么性质。
那么想法就是能不能预处理一些东西支持快速操作,或者把一些 \((c,d)\) 放在一起处理。
可以发现的无论我们怎么操作 \(c + d\) 的值都不会变,所以可以考虑将 \(c+d\) 一样的 \((c,d)\) 先放在一起处理,看看有什么性质。
可以发现的是如果在某一次操作中 \(c\) 或 \(d\) 达到了其能达到的上界或下界,那么无论原来的 \((c,d)\) 是什么,最后操作的答案都是一样的。
显然的一点就是不用考虑 \(d\) 达到上下界,因为 \(c\) 达到上界意味着 \(d\) 达到下界,\(c\) 达到下界意味着 \(d\) 达到上界。
那么如果我们按 \(c\) 排序之后会发现,能达到下界的 \((c,d)\) 对应序列的一段前缀,能达到上界的 \((c,d)\) 对应序列的一段后缀,所以这一段前缀对应的答案相同,这一段后缀对应的答案相同,分别暴力做一遍即可,当然如果前后缀有交,则所有的满足条件的 \((c,d)\) 答案都相同。
而对于既没有达到上界也没有达到下界的 \((c,d)\),就是直接 \(\sum v_i\) 就是对应的影响,因为没有任何一次操作使得这个操作实际的水的变化量不等于 \(v\)。
那么我们该怎么找这个前后缀呢,我们设 \(c\) 的上下界为 \([l,r]\),对于一段操作使得 \(c\) 的变化量为 \(\Delta c\),那么如果前面完全没有达到上下界的话,这个时候的 \(c\) 就是 \(c + \Delta c\) 如果此时满足 \(c + \Delta c \le l\) 也就是意味着 \(c\) 即使前面的操作没有达到上下界,也会在此时达到下界。
也就是说如果 \(c \le l - \Delta c\) 则 \(c\) 会达到下界,\(c \ge r - \Delta c\) 则 \(c\) 会达到上界。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int p[N][N],v[10 * N];
int solve(int a,int b,int c,int d,int n){
for(int i=1; i<=n; i++){
if(v[i] > 0){
int tmp = min(c,min(v[i],b - d));
c -= tmp,d += tmp;
}
else{
int tmp = min(d,min(-v[i],a - c));
c += tmp,d -= tmp;
}
}
return c;
}
int main(){
int n,a,b;scanf("%d%d%d",&n,&a,&b);
for(int i=1; i<=n; i++) scanf("%d",&v[i]);
for(int i=0; i<=a+b; i++){ //这里都是对于 A 桶来计算的
int l = max(0,i - b),r = min(a,i); // l,r 是对应的上下界
int L = l,R = r; //L,R 就是前后缀
int sum = 0; //A 桶的变化量
for(int j=1; j<=n; j++){
sum -= v[j];
L = max(L,l - sum);
R = min(R,r - sum);
}
if(L > R){
int ans = solve(a,b,l,i-l,n);
for(int j=l; j<=r; j++) p[j][i-j] = ans;
}
else{
int t1 = solve(a,b,L,i-L,n);
int t2 = solve(a,b,R,i-R,n);
for(int j=l; j<L; j++) p[j][i-j] = t1;
for(int j=L; j<=R; j++) p[j][i-j] = j + sum;
for(int j=R+1; j<=r; j++) p[j][i-j] = t2;
}
}
for(int i=0; i<=a; i++){
for(int j=0; j<=b; j++){
printf("%d ",p[i][j]);
}
printf("\n");
}
return 0;
}
F.Traveling in Berland
题目描述:
- 你在一个有 \(n\) 个点的环上顺时针前进,环上第 \(i\) 个点有两个权值 \(a_i,b_i\),分别代表从点 \(i\) 到点 \(i+1 \bmod n\) 的消耗的油和点 \(i\) 处的油价。你的车的油箱最多只能装 \(k\) 升油,对于每个点,求出从这个点开始顺时针转一圈返回这个点的最小消耗。
- \(3 \leq n \leq 2\times 10^5,1\leq k \leq 10^9,1\leq a_i\leq k,1\leq b_i\leq2\)
题目分析:
发现这个过程没啥性质,所以考虑是不是可以预处理一些东西使得可以快速处理,或者把一些情况相同的放在一起处理。
首先环的处理可以考虑二倍环长,发现一个关键点就是 \(b_i \in [1,2]\),也就是我们有贪心策略每一次在 \(b_i = 1\) 的地方尽可能多买,然后没办法了再从 \(b_i = 2\) 的地方买。
设 \(b_i = 1\) 的位置构成序列 \(p\) 所以可以考虑按 \(p\) 将序列分段,一个想法就是直接处理从 \(p_i\) 到 \(p_{i+1}\) 的最优花费,这样直接把两端拼起来看上去就很可以,但是拼起来这个事情必须要求我们在结尾的时候油量为 \(0\),这个要求其实就是不浪费油量很好做。
那么就是考虑这个最优花费怎么做,设 \(s\) 表示这一段花费的油量,则:
- 若 \(s \le k\),那么只需要在 \(b=1\) 的位置买 \(s\) 个即可
- 若 \(s > k\),则只能在 \(b=1\) 的位置买 \(k\) 个,然后在 \(b=2\) 的位置买完剩下的 \(s-k\) 个即可。
所以对于从 \(l\) 开始走走到 \(l+n\),我们可以找到 \(L,R\) 表示最靠近 \(l\) 的 \(b = 1\) 的位置,最靠近 \(l+n\) 的 \(b=1\) 的位置,这样我们就可以将贡献拆为:\([l,L]\),\([L,R]\),\([R,l+n]\),第一个和第三个显然可以 \(O(1)\),第二个的贡献可以通过我们预处理的贡献数组,然后通过双指针 \(O(n)\) 维护。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
int a[2 * N],b[2 * N],p[2 * N];
vector<int> v;
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
int n,k;scanf("%lld%lld",&n,&k);
int tmp = 100;
for(int i=1; i<=n; i++) scanf("%lld",&a[i]),a[i+n] = a[i];
for(int i=1; i<=n; i++) scanf("%lld",&b[i]),tmp = min(tmp,b[i]),b[i+n] = b[i];
for(int i=1; i<=2 * n; i++) a[i] += a[i-1];
for(int i=1; i<=2*n; i++){
if(b[i] == 1) v.push_back(i);
}
for(int i=0; i + 1 < v.size(); i++){
p[i] = a[v[i+1]-1] - a[v[i]-1];
if(p[i] > k) p[i] = (p[i] - k) * 2 + k * 1;
else p[i] = p[i] * 1;
}
if(tmp == 2){
for(int i=1; i<=n; i++) printf("%lld ",a[n] * 2);
printf("\n");
}
else{
int l=0,r=0,ans = 0;
for(int L=1; L<=n; L++){
int R = L + n;
while(r + 1 < v.size() && v[r+1] <= R) ans += p[r],++r;
while(v[l] < L) ans -= p[l],++l;
int tmp = ans;
tmp += (a[v[l]-1] - a[L-1]) * 2;
int res = a[R-1] - a[v[r] - 1];
if(res > k) tmp += (res - k) * 2 + k * 1;
else tmp += res * 1;
printf("%lld ",tmp);
}
printf("\n");
}
v.clear();
for(int i=1; i<=2 * n; i++) a[i] = 0;
for(int i=0; i<v.size(); i++) p[i] = 0;
}
return 0;
}
G.Prediction
题目描述:
有 \(n\) 个人,每个人有能力值 \(a_i\)。规定一个比赛顺序 \(p_1,p_2,\dots,p_n\),第一场 \(p_1,p_2\) 比,胜者与 \(p_3\) 比,胜者与 \(p_4\) 比……,共进行 \(n-1\) 轮比赛决出最终胜者。若能力值为 \(x,y\) 的两个人比赛,若 \(|x-y|>k\) 则能力值高者胜;否则两人都有可能胜出。请计数比赛顺序的方案数,使得任意一次比赛都有唯一胜者。
题目分析:
注意题目里是对于任意一次比赛都有唯一的胜者,而显然既然是唯一的,则必然是两个人中的最大值,所以本质上我们就是对满足如下条件的排列 \(p\) 计数:
那么就可以考虑 \(dp\) 来插入数,也就是按照从大到小或者从小到大的顺序插入 \(a_i\)。
如果从小到大插入,也就是说我们每一次插入都会使得一些位置的前缀 \(\max\) 发生改变,看上去就有点难以维护。
如果从大到小插入,发现其不会对任何的前缀 \(\max\) 产生影响,所以比较有前途,也就是可以直接设 \(dp_i\) 表示插入了 \((i,n]\) 的情况下合法的方案数,下面就是分类讨论一下转移:
- 若将 \(a_i\) 插入到第一个位置,设 \(lst_i\) 表示第一个满足 \(a_i - a_{lst_i+1} > k\) 的位置,则对于 \((lst_i,i)\) 其必然只能插入到第二个位置之后,也就是:\(dp_{lst_i} + dp_i\text{A}_{n-lst_i-2}^{lst_i-i-1} \to dp_{lst_i}\)
- 若将 \(a_i\) 插入到其它的位置,因为第一个转移的限制,所以其实插入的哪里都可以,也就是 \(dp_i \times (n-i) \to dp_{lst_i}\)
代码:
点击查看代码
#include <stdio.h>
typedef long long ll;
const int mod = 998244353;
int a[1000007], lst[1000007];
ll fac[1000007], inv_fac[1000007], dp[1000007];
inline ll quick_pow(ll x, ll p, ll mod){
ll ans = 1;
while (p){
if (p & 1) ans = ans * x % mod;
x = x * x % mod;
p >>= 1;
}
return ans;
}
inline void init(int n){
fac[0] = 1;
for (int i = 1; i <= n; i++){
fac[i] = fac[i - 1] * i % mod;
}
inv_fac[n] = quick_pow(fac[n], mod - 2, mod);
for (int i = n - 1; i >= 0; i--){
inv_fac[i] = inv_fac[i + 1] * (i + 1) % mod;
}
}
inline int read(){
int ans = 0;
char ch = getchar();
while (ch < '0' || ch > '9'){
ch = getchar();
}
while (ch >= '0' && ch <= '9'){
ans = ans * 10 + (ch ^ 48);
ch = getchar();
}
return ans;
}
inline ll arrange(int n, int m){
if (m == 0) return 1;
if (n < 0 || m < 0 || n < m) return 0;
return fac[n] * inv_fac[n - m] % mod;
}
int main(){
int n = read(), k = read();
init(n);
for (int i = 1; i <= n; i++){
a[i] = read();
}
for (int i = 1; i <= n; i++){
lst[i] = lst[i - 1];
while (a[i] - a[lst[i] + 1] > k) lst[i]++;
}
dp[n] = 1;
for (int i = n; i >= 1; i--){
dp[i - 1] = (dp[i - 1] + dp[i] * (n - i) % mod) % mod;
dp[lst[i]] = (dp[lst[i]] + dp[i] * arrange(n - lst[i] - 2, i - lst[i] - 1) % mod) % mod;
}
printf("%lld", dp[0]);
return 0;
}