Codeforces Round 876 题解
Codeforces Round 876 (Div. 2)
A. The Good Array
标签
greedy
math
题意
对于任意 \(i\in\{1,2,\dots,n\}\),要求数列 \(a\) 满足前 \(i\) 个元素中至少有 \(\lceil\frac{i}{k}\rceil\) 个元素为 \(1\),后 \(i\) 个元素中至少有 \(\lceil\frac{i}{k}\rceil\) 个元素为 \(1\)。
思路
- 考虑特殊情况 \(k=1\),因为此时序列 \(1\) 的插入点连续,所以答案为 \(n\)。
- 欲令数列 \(a\) 满足前 \(i\) 个元素中至少有 \(\lceil\frac{i}{k}\rceil\) 个元素为 \(1\),根据贪心只需要在 \(1, k+1, 2k+1, \dots,xk+1,n\) 的位置插入 \(1\)。在该前提下,讨论欲满足另一要求需要额外插入的 \(1\) 的个数,分类讨论。
- 若 \(xk+1\) 与 \(n\) 之间的差值 \(d=n\%k-1> 0\),则易得 \(0<d<k\),此时无需再多插 \(1\),答案为 \(\lfloor\frac{n-1}{k}\rfloor+2\);若 \(d=0\),则说明 \(xk+1\) 与 \(n\) 重合,而 \(n\) 又为满足第二要求的第一个 \(1\) 的位置,故此时无需多插 \(1\),进而答案为 \(\lfloor\frac{n-1}{k}\rfloor+1\);若 \(d=-1\),则说明 \(xk+1=n+1\),此时把 \((x-1)k+1\) 作为新的 \(x'k+1\),答案仍为 \(\lfloor\frac{n-1}{k}\rfloor+2\)。
4, 时间复杂度为 \(\mathcal O(t)\)。
代码
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
using namespace std;
int t, n, k;
int main ()
{
scanf ("%d", &t);
while (t --)
{
scanf ("%d %d",&n, &k);
if (k == 1) printf ("%d\n", n);
else if (n % k - 1 != 0)
printf ("%d\n", (n - 1) / k + 2);
else printf ("%d\n", (n - 1) / k + 1);
}
return 0;
}
收获
- 脑子混乱时,静下心来重新发现性质。比如,该题最关键的性质为除去位置 1 和位置 n 处的 \(1\),其他位置的 \(1\) 之间的差值恒为 \(k\),以及对是否需要额外插 \(1\) 的判断条件。
B. Lamps
标签
greedy
sortings
思路
- 考虑当 \(a_i=c,i\in\{1,2,\dots,n\},c为常量\) 的情形。对 \(a\) 进行排序,使得 \(a_1\ge a_2\ge\dots\ge a_n\)。易得,此时的答案为 \(\sum_{i=1}^{\min(n,c)}\)。
- 考虑一般的情形。令 \(c_k\) 表示 \(\operatorname{card}(\{d_{k_j}|a_i=k\}),k=1,2,\dots,n\)。对 \(d_k\) 进行由大到小的排序,则由 1 可知:\(s_k=\max\{\sum_{i=1}^{c_k}(d_{k_{j_i}}[i\le \min(c_k, k)])\}=\sum_{i=1}^{\min(k,c_k)}d_{k_i}\)。进而总可以通过先点亮 \(a\) 值小的灯泡,使得最后的答案为 \(\sum_{i=1}^{n}s_i\)。
- 时间复杂度为 \(\mathcal O (n\log n)\)。
代码
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 100;
struct worker
{
int a, b;
} e[maxn];
bool cmp (worker a, worker b)
{
if (a.a == b.a) return a.b > b.b;
return a.a < b.a;
}
int n, t, tot[maxn];
int main ()
{
scanf ("%d", &t);
while (t--)
{
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
tot[i] = 0;
for (int i = 1; i <= n; i++)
scanf ("%d%d", &e[i].a, &e[i].b),
tot[e[i].a] += 1, tot[e[i].a] = min (tot[e[i].a], e[i].a);
sort (e + 1, e + n + 1, cmp);
long long ans = 0;
for (int i = 1; i <= n; i++)
{
if (e[i].a == e[i-1].a) continue;
for (int j = i; j <= i + tot[e[i].a] - 1; j ++)
ans += e[j].b;
}
printf ("%lld\n", ans);
}
return 0;
}
收获
- 从最简单的情形出发。本题即是从 \(a\) 为常数列出发。
- 分析本题重要的性质,即灯泡可以再亮后被毁坏,使得亮的灯泡变少。
C. Insert Zero and Invert Prefix
标签
constructive algorithms
思路
- 首先考虑最特殊的两种情形。当 \(a_i\equiv 0\) 时,只需令 \(p_i\equiv 0\) 即可,故可考虑把连续的 \(0\) 统一处理。当 \(a_i\equiv 1\) 时,无法构造,继续观察以 \(1\) 为结尾的 \(a\) 序列都无法被构造出,原因为 \(1\) 的产生是由于有 \(0\) 在新插入的 \(0\) 前面,故 \(1\) 不可作为结尾。
- 考虑如何构造序列 \(b\)。因为连续的 \(1\) 可以由在连续的 \(0\) 添加新 \(0\) 得到,故可以把类似于 \(1,1,\dots,1,0,0,\dots,0\) 的数串作为一个整体,特别地,要注意特殊串 \(0,0,\dots,0\) 与 \(1,1,\dots,1\)。又因为 \(0\) 的插入只影响其前缀,故可以以序列 \(a\) 从 \(n\) 到 \(1\) 的顺序,按照划分开的数串,以从 \(1\) 到 \(n\) 的顺序,构造 \(b\)。具体的方式为,对于每个数串 \(S\),\(b\) 以 FIFO 的方式,先压入 \(|S|-1\) 个 \(0\),再压入一个 \(|\{x|a_x=1,a_x\in S\}|\)。
- 时间复杂度为 \(\mathcal O(n)\)。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
using namespace std;
const int maxn = 1e5 + 100;
int t, n, a[maxn], b[maxn];
int main ()
{
scanf ("%d", &t);
while (t --)
{
scanf ("%d", &n);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
if (a[n] == 1)
{
printf ("NO\n");
continue;
}
else
{
printf ("YES\n");
int p = n, pi = 0;
while (p)
{
int pend = -1, pendi = -1;
for (int i = p; i >= 1; i--)
if (a[i] != 0)
{
pend = i + 1;
break;
}
if (pend == -1)
{
for (int i = p; i >= 1; i--)
b[++ pi] = 0;
break;
}
for (int i = pend - 1; i >= 1; i--)
if (a[i] != 1)
{
pendi = i + 1;
break;
}
if (pendi == -1)
{
for (int i = 1; i < p; i++)
b[++ pi] = 0;
b[++ pi] = pend - 1;
break;
}
for (int i = pendi; i < p; i++)
b[++ pi] = 0;
b[++ pi] = pend - pendi;
p = pendi - 1;
}
for (int i = 1; i <= n; i++)
printf ("%d ", b[i]);
printf ("\n");
}
}
return 0;
}
D. Ball Sorting
标签
data structures
dp
sortings
思路
- 考虑 \(0\) 的作用。\(0\) 可以实现将与其邻近的两个数通过花费相应的代价调用到数列中的任意位置,根据贪心的想法,这些调换后的数与未发生调换的数一定可以满足 \(c\) 升序。
- 故可考虑 \(dp(i,j)\) 表示不选第 \(i\) 个数,有 \(j\) 个 \(0\) 使 \(1\) 到 \(i\) 的数列的 \(c\) 升序所花费的最小代价。则需要有初始化:\(c_{n+1}=n+1;c_{0}=0;dp(i,0)=0,i=1,2,\dots,\max\{k|c_{1,2,\dots,k}升序\}\)。转移方程为,若选择不使用第 \(j\) 个 \(0\),花费为 \(dp(i,j-1)\);若选择使用第 \(j\) 个 \(0\):若 \(j\ne i-1\),则花费的代价为 \(\min_{k\in\{1,2,\dots,i-1\}}\{dp(k,j-1)+i-k-1\}\);若 \(j=i-1\),则花费的代价为 \(dp(i-1,j)\),取最小代价即可。答案便为 \(dp(n+1,k)\)。
- 时间复杂度为 \(\mathcal O(tn^3)\)。
代码
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
using namespace std;
const int maxn=510;
int t,n,c[maxn],dp[maxn][maxn];
int main ()
{
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&c[i]);
}
c[0]=0,c[n+1]=n+1;
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n+1;i++)
{
if(c[i-1]<c[i]) dp[i][0]=0;
else break;
}
for(int i=1;i<=n+1;i++)
for(int j=1;j<=i;j++)
{
dp[i][j]=min(dp[i][j],dp[i][j-1]);
for(int k=0;k<i;k++)
if(c[k]<c[i])
{
if(k<i-1) dp[i][j]=min(dp[i][j],dp[k][j-1]+i-k-1);
else dp[i][j]=min(dp[i][j],dp[k][j]);
}
}
for(int i=1;i<=n;i++)
printf("%d ",dp[n+1][i]);
printf("\n");
}
return 0;
}