Educational CF Round 135 题解
签到题,找最大的那堆就行了
签到题,要让最后剩下的后缀和最大,首先\(n\)应该在最后,那么\(n\)前面最大的子段和只能是\(n-1\),因此答案一定是\(2n-1\)。接下来重新排列前面的部分,保证在\(n-1\)之前的子段和是\(0\)就行了。那么不妨让它每隔一个就是\(0\),那么每两个数翻转一下——\(2,1,4,3 ...\)(偶数个)或\(1,3,2,5,4...\)(奇数个)
题意:\(f(x)\)表示\(x\)在十进制下的位数。有两个数组\(a\)和\(b\),每次操作可以将\(a_i\)变成\(f(a_i)\)或\(b_i\)变成\(f(b_i)\)。当两个数组按大小排序能变成相同数组时,称两个数组相似。问原始的\(a\)和\(b\)数组经过最少多少次操作能变成相似。
分析:首先这两个数组一定可以相似,因为所以数字最终通过\(f\)函数都能变成1。那么可以每次取两个数组里的最大值,如果相等那么这两个元素可以匹配,并删除。否则一定有某一个最大值找不到匹配,可以应用\(f(x)\)将其变小,插入到原数组中,直到变空。
核心代码
std::multiset<int> a, b;
void Main()
{
int n;
read(n);
for (int i = 1, x; i <= n; ++i)
read(x), a.insert(x);
for (int i = 1, x; i <= n; ++i)
read(x), b.insert(x);
int ans = 0;
while (!a.empty())
{
if (*a.rbegin() == *b.rbegin())
{
a.erase(std::prev(a.end())), b.erase(std::prev(b.end()));
continue;
}
else
{
++ans;
if (*a.rbegin() > *b.rbegin())
{
int x = *a.rbegin();
a.erase(std::prev(a.end()));
a.insert(log10(x) + 1);
}
else
{
int x = *b.rbegin();
b.erase(std::prev(b.end()));
b.insert(log10(x) + 1);
}
}
}
printf("%d\n", ans);
}
题意:A和B从一个字符串\(s\)里取字符,每次只能从两端取,取出的字符放到自己字符串的前面。最终字典序小的那个人胜。字符串长度不超过\(2000\)。
分析:
经典博弈论区间DP题型。
比较字符串的字典序就是找到更小的前缀,而字符串的前缀总是从更小的区间得到。
设\(dp[i][j]\)表示区间\([i,j]\)的字符串,当前对A最优的结果。那么这一轮A的决策是取\(s[i]\)或者\(s[j]\)。
假设A取了\(s[i]\),那么B面对的局面是\([i+1,j]\),B的决策是取\(s[i+1]\),和\(s[j]\);下一轮留给A的局面分别是\([i+2,j]\)和\([i+1,j-1]\)。因为A和B都是按最优方法执行,因此A如果取\(s[i]\),则下一轮A面对的局面一定是\([i+2,j]\)和\([i+1,j-1]\)之间的最差局面。
也就是:
\(worse(add(dp[i+1][j-1], s[i]<s[j]), add(dp[i+2][j], s[i] < s[i+1]))\)(取\(s[i]\))
add函数表示将两个比较结果合并的结果。
取\(s[j]\)的情况同理。对于A本身来说,肯定是选择两个决策中最好的那个,因此状态转移方程是:
时间复杂度\(O(n^2)\)
核心代码
const int MAXN = 2000;
int dp[MAXN + 5][MAXN + 5];
char s[MAXN + 5];
int worst(int x, int y)
{
if (x == 0 || y == 0)
return 0;
if (x == 2 || y == 2)
return 2;
return 1;
}
int better(int x, int y)
{
if (x == 1 || y == 1)
return 1;
if (x == 2 || y == 2)
return 2;
return 0;
}
int add(int x, int y)
{
if (x == 1)
return 1;
if (x == 0)
return 0;
return y;
}
int cmp(char x, char y)
{
if (x < y)
return 1;
if (x > y)
return 0;
return 2;
}
void Main()
{
scanf("%s", s + 1);
memset(dp, -1, sizeof dp);
int n = strlen(s + 1);
for (int i = 1; i <= n - 1; ++i)
dp[i][i + 1] = s[i] == s[i + 1] ? 2 : 1;
for (int len = 4; len <= n; len += 2)
{
for (int i = 1; i <= n && (i + len - 1) <= n; ++i)
{
int j = i + len - 1;
dp[i][j] = 0;
// choose i
int tmp1 = worst(add(dp[i + 2][j], cmp(s[i], s[i + 1])), add(dp[i + 1][j - 1], cmp(s[i], s[j])));
int tmp2 = worst(add(dp[i + 1][j - 1], cmp(s[j], s[i])), add(dp[i][j - 2], cmp(s[j], s[j - 1])));
dp[i][j] = better(tmp1, tmp2);
// printf("dp[%d][%d] = %s\n", i, j, dp[i][j] == 0 ? "Lose" : (dp[i][j] == 1 ? "Win" : "Draw"));
}
}
if (dp[1][n] == 2)
printf("Draw\n");
else if (dp[1][n] == 0)
printf("Bob\n");
else
printf("Alice\n");
}
题意:
有\(n\)道菜,第i道菜放黑胡椒的美味度是\(a[i]\),放白胡椒的美味度是\(b[i]\)。有m个商店出售胡椒包,第\(i\)个商店的黑胡椒包有\(x[i]\)份黑胡椒,白胡椒包有\(y[i]\)份白胡椒。对于每个商店,如果只在该商店购买胡椒包,且完全不浪费的情况下(购买\(u\)份黑胡椒包和\(v\)份白胡椒包,使得\(u*x[i]+v*y[i]=n\)),做菜的美味度最大是多少。\(n,m ≤ 3*10^5\)
分析:
看到商店胡椒包关于不浪费的描述,很显然能想到扩展欧几里得,这样可以得到在该商店购买胡椒包是否可行。如果可行,方程\(u*x[i]+v*y[i]=n\)的正整数通解就是购买方案,接下来就是从这些方案里找到美味度最大的那个方案。
假设一共要做\(x\)道白胡椒菜,那么一共要做\(n-x\)道黑胡椒菜。
美味度\(S(x) = \sum_{i = 1}^x{b_i}+\sum_{j=1}^{n-x}{a_j}\)
展开之后变成
\(S(x) = \sum_{i = 1}^x{(b_i - a_i)} + \sum_{i=1}^n{a_i}\)
这个式子后半部分是定值,所以只需要最大化前半部分就可以了,发现这就是将\(b_i-a_i\)从大到小排序之后的前缀和。
还可以发现,由于将\(b_i-a_i\)排序,所以\(S(x)\)是单峰函数,峰值在前缀和最大值处。
回到问题本身,现在有一系列\(v\)的通解形式和一个对于\(v*y[i]\)的单峰函数。找到最大值只需要考虑两个位置:以峰值为分界线,前半峰里面\(v\)的通解的中\(v*y[i]\)最大值以及后半峰里面\(v\)的通解中\(v*y[i]\)的最小值。
核心代码
const int MAXN = 3E5;
int diff[MAXN + 5];
ll sum[MAXN + 5];
int exgcd(int a, int b, ll &x, ll &y)
{
if (b == 0)
return x = 1, y = 0, a;
int d = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - (a / b) * y;
return d;
}
void Main()
{
int n, m;
read(n);
ll tot = 0;
for (int i = 1, a, b; i <= n; ++i)
{
read(a, b);
tot += a;
diff[i] = b - a;
}
std::sort(diff + 1, diff + n + 1, std::greater<int>());
sum[0] = 0;
for (int i = 1; i <= n; ++i)
sum[i] = sum[i - 1] + diff[i];
int mx = 0;
for (int i = 1; i <= n; ++i)
if (sum[i] > sum[mx])
mx = i;
read(m);
for (int i = 1, a, b; i <= m; ++i)
{
read(a, b);
ll x, y;
int d = exgcd(a, b, x, y);
if (n % d != 0)
printf("-1\n");
else
{
int s = n / d;
x *= s, y *= s;
x = (x % (b / d) + b / d) % (b / d);
y = (n - 1ll * a * x) / b;
if (y < 0)
{
printf("-1\n");
continue;
}
int tmax = y / (a / d), ymin = y - tmax * (a / d);
int bymin = b * ymin, bymax = b * y;
ll ans = -1;
if (mx <= bymin)
ans = tot + sum[bymin];
else if (mx >= bymax)
ans = tot + sum[bymax];
else
{
int k = 1ll * (mx - bymin) * d / (1ll * a * b);
ans = std::max(tot + sum[bymin + 1ll * a * b / d * k],
tot + sum[bymin + 1ll * a * b / d * k + 1ll * a * b / d]);
}
printf("%lld\n", ans);
}
}
}
题意
有一个长度为\(n\)的数组\(a\),将其按某种顺序重新排列成数组\(p\),生成数组\(b\),使得\(b[i]\)是\(p[i]\)的倍数且\(b[i]\)单调递增。求\(b[i]\)数组的和最小值。\(n≤1000\)
分析
其实这个题就是把每个\(a[i\)],让\(b[i]\)是它的一个倍数即\(b[i] = k * a[i]\),且\(b[i]\)是唯一的。所谓按某种顺序重新排列\(a\),让\(b\)单调递增,就是把\(b\)排个序就好了,所以这一句话的作用就在于揭示了\(b[i]\)是唯一的。
问题在于\(a[i]\)和\(a[j]\)的倍数可能相同,即存在\(k_1 * a[i] = k_2 * a[j]\),但\(k_1*a[i]\)只能用一次。
不难想到网络流模型,由\(a[i]\)连向它所有的倍数\(k * a[i]\),容量为1;源点向\(a[i]\)连边,容量为1;倍数\(k*a[i]\)向汇点连边,容量为1,费用为\(k*a[i]\)。跑一遍最小费用最大流即可。
这样建出来是一个二分图,左部有\(n\)个节点,右部最多有\(n^2\)个节点,一共有\(n^2\)条边。这样跑费用流是爆炸TLE的。
考虑费用流的过程,只有右部节点向汇点边有费用,那么按最短路增广的话肯定是优先费用小的,也就是增广的顺序右部节点按权值从小到大排序。
那么不妨将脱离费用流,直接按右部节点从小到大排序,做增广顺序,跑二分图匹配。匈牙利算法的时间复杂度是\(O(VE)=O(n^4)\),还是严重超时。
其实这里左部和右部的节点数量差距很大,最大匹配也就是n,而对右部节点按顺序增广有很多冗余。不妨在匈牙利算法没有找到增广路时,保留访问标记。因为当没有找到增广路的时候,图的匹配没变化,当前访问节点i没有找到增广路,下一次访问节点i也不会找到增广路。这样的话每两次最大匹配+1的时候,访问\(O(n^2)\)条边,那么总复杂度是\(O(ME)=O(n^3)\),再加上图匹配实际跑起来的效率很高,其实可以通过这道题目。
其实还有很多种可能的优化,比如动态加点、动态删边等,这是一道上限很高的题目,因此它的难度分数在3100。
核心代码
constexpr int MAXN = 1E3;
std::vector<int> G[MAXN * MAXN + MAXN + 5];
bool vis[MAXN + 5];
int n, a[MAXN + 5], b[MAXN * MAXN + 5];
int link[MAXN + 5];
bool find(int u)
{
for (int v : G[u])
{
if (!vis[v])
{
vis[v] = true;
if (link[v] == 0 || find(link[v]))
return link[v] = u, true;
}
}
return false;
}
void Main()
{
read(n);
for (int i = 1; i <= n; ++i)
read(a[i]);
int cnt = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
b[++cnt] = a[i] * j;
std::sort(b + 1, b + cnt + 1);
cnt = std::unique(b + 1, b + cnt + 1) - (b + 1);
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= n; ++j)
{
int u = std::lower_bound(b + 1, b + cnt + 1, a[i] * j) - (b);
G[u].push_back(i);
}
}
ll ans = 0;
for (int i = 1; i <= cnt; ++i)
{
if (find(i))
{
ans += b[i];
for (int j = 1; j <= n; ++j)
vis[j] = false;
}
}
printf("%lld\n", ans);
}
题意:
在一个长度为\([0,d]\)的线段上,有\(n\)盏灯,位置是\(l[i]\)。有m个感兴趣的点\(p[i]\)。一盏灯的功率是\([0,d]\),如果一个位置\(y\)被位置为\(x\),功率为\(p\)的灯照亮,那么有\(|y-x|≤p\)
有\(q\)次询问,每次询问都要加上一盏位置在\(f\)的灯,求将所有感兴趣的点照亮的灯的功率分配的方案数。询问之间互相独立。
\(4≤d≤3⋅10^5; 1≤n≤2⋅10^5; 1≤m≤16;1≤q≤5⋅10^5\)
分析:
先不考虑加灯。
看到m这么小的取值,很自然地想到状态压缩。设\(dp[i][S]\)表示前i盏灯照亮集合S的灯的方案数,\(w[i][s]\)表示灯i照亮集合为S的方案数,那么有\(dp[i][S] = dp[i-1][S']*w[i][s_0](S'|s_0 = S)\)
这样做时间复杂度过高了。注意到一个很棘手的点在于,一个位置可能被重复照亮,这样在状态转移的时候不是很好枚举。
正难则反,不妨考虑一下容斥。
首先很显然,总方案数是\((d+1)^n\)。
不合法的方案则是某几盏灯不亮,其他灯随意。假设\(dp[S]\)表示不亮的灯是S的方案数,那么最终总的不合法方案数是\(\sum_{S=1}^{2^m-1}(-1)^{|S|}*dp[S]\),并且显然\(dp[0] = (d+1)^n\)。
那么最终答案就是\(\sum_{S=0}^{2^m-1}(-1)^{|S|}*dp[S]\)
所以接下来考虑\(dp[S]\)的计算方法。
注意到一盏灯,其实只会照亮以该灯为中心,一段距离内的所有兴趣点。假设\(S\)里相邻的两个1是\(S_i\)和\(S_{i+1}\),那么这两个兴趣点不被照亮的方案一定是这两个兴趣点内所有的灯都没有覆盖到这两个点。
设\(g[l][r]\)表示兴趣点\(l\)和\(r\)内的灯没有覆盖到\(l\)和\(r\)的方案数,这个数组可以在\(O(nm^2)\)的时间内预处理出来。
为了方便计算,我们添加\(l[0]=-inf,l[m+1]=inf\)两个点,那么\(dp[S] = \prod_{i=1}^{|S|-1}g[S_i][S_{i+1}]*g[0][S_1]*g[S_{|S|}][m+1]\)。
这一步预处理时间复杂度为\(O(m2^m)\)。
现在考虑添加一个灯\(f\)。
如果从头再做一次dp,时间复杂度显然无法接受。
注意到添加灯的时候,对\(g\)数组的修改可以在\(O(m^2)\)的时间复杂度完成。那么有没有一种快速的方法求出\(\sum_{S=0}^{2^m-1}(-1)^{|S|}*dp[S]\)的值呢?
事实上是存在的。类比背包问题,每个物体可选可不选,如果用状态压缩去算的话显然也是枚举每种集合。实际上我们可以一位一位地递推。
重新设置dp状态,设\(dp[i]\)为长度为i的二进制串,且第i位为1的方案总数,那么有递推式
\(dp[i] = \sum_{j=0}^{i-1}(-1)*dp[j]*g[j][i]\)。注意到每次末尾多一个1的情况下,对前面的状态中-1的奇偶性会出现变化,因此这里直接多乘一个-1就可以了。
由于我们知道第\(0\)位和第\(m+1\)位肯定为1,那么\(dp[0]=-1\),最终答案就是\(dp[m+1]\)
这样做的话dp的时间复杂度是\(O(m^2)\),在每次添加灯的情况下可以直接重新做dp,查询的时间复杂度\(O(qm^2)\)。
最终时间复杂度为\(O(nm^2 + qm^2)\)
除此之外还有很多做法。例如官方题解中用莫比乌斯反演在添加一盏灯的时候直接对相关的\(dp[S]\)求出贡献。或者在添加一盏灯的时候,被它照亮的一定是中间的一段,那么前缀和后缀必须是被原有的灯照亮。因此可以利用容斥预处理前缀\([1,l]\)和后缀\([r,n]\)都被原有的灯照亮的方案数。
总的来说这道题还是很灵活的,充分利用了很多组合数学和动态规划中的知识。
核心代码
constexpr int MAXN = 2E5;
constexpr int MAXD = 3e5;
constexpr int MAXM = 16;
int g[MAXM + 5][MAXM + 5], sg[MAXM + 5][MAXM + 5], dp[MAXM + 5];
int l[MAXN + 5], p[MAXM + 5];
void Main()
{
int d, n, m;
read(d, n, m);
for (int i = 1; i <= n; ++i)
read(l[i]);
for (int i = 1; i <= m; ++i)
read(p[i]);
std::sort(l + 1, l + n + 1);
std::sort(p + 1, p + m + 1);
p[0] = -d * 10, p[m + 1] = d * 10;
ll all = 1;
for (int i = 1; i <= n; ++i)
all = (all * (d + 1)) % mod;
for (int i = 0; i <= m + 1; ++i)
for (int j = 0; j <= m + 1; ++j)
g[i][j] = 1;
for (int i = 1, L = 0; i <= n; ++i)
{
while (L <= m && p[L + 1] < l[i])
++L;
int R = L + 1;
for (int x = 0; x <= L; ++x)
for (int y = R; y <= m + 1; ++y)
{
int dist = std::min(std::abs(l[i] - p[x]) - 1, std::abs(l[i] - p[y]) - 1);
dist = std::min(d, dist);
g[x][y] = 1ll * (dist + 1) * g[x][y] % mod;
}
}
int q;
read(q);
while (q--)
{
int f, L = 0;
read(f);
while (L <= m && p[L + 1] < f)
++L;
int R = L + 1;
dp[0] = mod - 1;
for (int x = 0; x <= L; ++x)
for (int y = R; y <= m + 1; ++y)
{
int dist = std::min(std::abs(f - p[x]) - 1, std::abs(f - p[y]) - 1);
dist = std::min(d, dist);
sg[x][y] = g[x][y];
g[x][y] = 1ll * (dist + 1) * g[x][y] % mod;
}
for (int i = 1; i <= m + 1; ++i)
{
dp[i] = 0;
for (int j = 0; j <= i - 1; ++j)
dp[i] = (dp[i] + mod - 1ll * dp[j] * g[j][i] % mod) % mod;
}
printf("%d\n", dp[m + 1]);
for (int x = 0; x <= L; ++x)
for (int y = R; y <= m + 1; ++y)
{
g[x][y] = sg[x][y];
}
}
}

浙公网安备 33010602011771号