个人思路,仅作记录,可以参考,欢迎交流。
比赛地址:传送门
A. Vasya and Coins
(第一眼看到题面想到了洛谷P3951,不过这两题的区别还是比较大的)
【题意】求用a枚1元硬币和b枚2元硬币不能表示的最小数额
【思路】用b枚2元硬币可以表示b*2内的任意偶数;而只要至少拥有一枚1元硬币,就可以由每个偶数加上这枚硬币得到b*2+1内的任意奇数,也就能够表示b*2+1内的所有整数,此时再考虑用上剩下的a-1枚1元硬币,最多表示到b*2+a。所以做法就是判断是否有1元硬币,若有则答案为b*2+a+1,若没有则答案为1。
【代码】
void solve()
{
int a, b;
cin >> a >> b;
cout << (a ? b * 2 + a + 1 : 1) << '\n';
return;
}
B. Vlad and Candies
【题意】给定一个非负整数组,每次选择最大项之一使其-1,判断能否在「避免连续两次选择同一项」的情况下将所有元素减为0
【思路】一旦当前最大项不止一个,就可以通过循环轮流选择的方式将所有元素归零,所以只要考虑最开始没有并列最大项时的情况。显然,如果初始时最大项只有一个且比次大项多出2或以上,答案就为NO,否则答案就为YES(数组只有一项时将0视作次大项)。
【代码】
int a[200005];
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
if (n == 1)
{
cout << (a[1] == 1 ? "YES\n" : "NO\n");
}
else
{
int m = *max_element(a + 1, a + n + 1);
nth_element(a + 1, a + n - 1, a + n + 1);
cout << (m - a[n - 1] <= 1 ? "YES\n" : "NO\n");
}
return;
}
C. Get an Even String
【题意】给定一个只含小写拉丁字母的字符串,求最少删去多少项才能使其满足「第奇数项与第奇数+1项相同」
【思路】该题n的规模为2e5,所以时间复杂度必须小于o(n^2)。考虑从左向右扫描的过程,每扫到一个字母,就再取下一个字母,若下一个字母与它相同则满足要求,一起跳过;若不相同,有两种解决方案:
- 删去前者,继续向下找与后者相同的字母
- 删去后者,继续向下找与前者相同的字母
这时候结合“只含小写拉丁字母”的提示可以想到采用二维动态规划,定义dp[i][j]表示「扫到第i位且上一个未配对的字母为j+'a'」的状态下最少已经删去的项数,再规定「j=26表示当前没有未配对的字母」。初始时dp[0][26]=0,扫到一个字符j时只要考虑四种状态转移:
- 上一状态未配对的字母为j,与其配对
- 上一状态没有未配对的字母,将j作为未配对的字母
- 上一状态没有未配对的字母,但将j删去
- 上一状态未配对的字母不为j,将j删去
最后取dp[n][26]即为答案,这样就可以o(n*27)解决(因为考虑到i时的状态只与i-1时的状态有关,所以使用滚动数组方法,开dp[2][27]即可)。
【代码】
void solve()
{
string s;
cin >> s;
int n = s.size();
s = '\0' + s;//使下标变为1到n,方便代码书写
int dp[2][30];//考虑到第i位,上一个未配对的字符为'a'+j
memset(dp, 0x3f, sizeof(dp));
dp[0][26] = 0;//j=26表示没有未配对的字符
for (int i = 1; i <= n; ++i)
{
for (int j = 'a'; j <= 'z'; ++j)
{
if (j == s[i])
{
dp[i % 2][j - 'a'] = dp[(i - 1) % 2][26];
}
else
{
dp[i % 2][j - 'a'] = dp[(i - 1) % 2][j - 'a'] + 1;
}
}
dp[i % 2][26] = min(dp[(i - 1) % 2][26] + 1, dp[(i - 1) % 2][s[i] - 'a']);
}
cout << dp[n % 2][26] << endl;
return;
}
D. Maximum Product Strikes Back
【题意】给定一个只含-2, -1, -0, -1, 2的数组,可以任意删除其最左端或最右端的项,求如何删除使得剩余项的乘积最大(规定空数组的乘积为1)
【思路】从题目可以提取分析出如下信息:
- 最坏情况下(删空)乘积至少也可以为1,所以最大乘积一定为正
- 因为数组只含这五种元素,所以删1对结果无影响,而删2可能使结果更小
所以分三种情况:
- 若原数组乘积为正,则无需删除,因为越删乘积只可能越小
- 若原数组乘积为负,则在删除的绝对值最小的情况下删去一个负数使得乘积为正
- 若原数组乘积为0,则把原数组以0和左右边界为界划为若干子数组,则子数组满足情况1或情况2,再找经过删除后乘积最大的一个子数组
其中第二种情况只要考虑从左删或从右删两种情况,比较哪种的剩余乘积更大;第三种情况也需要比较每个子数组处理后的乘积。而在计算子数组乘积时如果简单地相乘,2^n显然会超限,所以只要记录2的次数即可。
【代码】
struct POS
{
int lef, rig;
};
int a[200005];
int CalMul(POS p)//计算子数组p的乘积
{
int res = 0;//2的次数
bool sign = 1;//乘积的符号
for (int i = p.lef; i <= p.rig; ++i)
{
if (a[i] < 0)
{
sign = !sign;
}
if (abs(a[i]) == 2)
{
res++;
}
}
return res * (sign ? 1 : -1);
}
POS MaxSub(int lef, int rig, int sign)//找最大子数组
{
if (sign > 0)//不删除
{
return { lef,rig };
}
else if (sign < 0)//比较从左删或从右删的结果,选一种
{
int lp = lef, rp = rig;
int lv = 1, rv = 1;
while (a[lp] >= 0)
{
lv += (a[lp] == 2);
lp++;
}
lv += abs(a[lp]) - 1;
while (a[rp] >= 0)
{
rv += (a[rp] == 2);
rp--;
}
rv += abs(a[rp]) - 1;
if (lv < rv)
{
return { lp + 1,rig };
}
else
{
return { lef,rp - 1 };
}
}
else
{
POS res = { 1,0 }, tmp;
int maxv = 0, tmpv;
sign = 1;
for (int i = lef; i <= rig + 1; ++i)
{
if (i <= rig && a[i] < 0)
{
sign *= -1;
}
else if (i == rig + 1 || a[i] == 0)//分割子数组,再把子数组重新传入MaxSub()
{
tmp = MaxSub(lef, i - 1, sign);
tmpv = CalMul(tmp);
if (maxv < tmpv)//找最大乘积
{
maxv = tmpv;
res = tmp;
}
lef = i + 1;//更新起点
sign = 1;//重置符号
}
}
return res;
}
}
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
int sign = 1;
for (int i = 1; i <= n; ++i)
{
if (a[i] < 0)
{
sign *= -1;
}
else if (a[i] == 0)
{
sign = 0;
}
}
POS res = MaxSub(1, n, sign);
cout << res.lef - 1 << " " << n - res.rig << endl;
return;
}
E. Matrix and Shifts
(这题难道不比C和D简单多了么,一下秒了)
【题意】给定一个二进制方阵,求最少要对多少个元素取反才能使矩阵满足「可以经过“变换”成为对角阵」,其中“变换”为:将 第一行/最后一行 置于 最后一行/第一行 的 下/上 方或将 第一列/最后一列 置于 最后一列/第一列 的 右/左 方,如:
┌—> 1 0 0 0 0
| 1 1 1 1 1 1 1 1 1 1
| 1 1 1 1 0 ==> 1 1 1 1 0
| 1 1 1 0 0 1 1 1 0 0
| 1 1 0 0 0 1 1 0 0 0
└—-1 0 0 0 0
【思路】矩阵的变换可以理解为在一个由原方阵拼成的无限大平面上用一个方框取矩阵,取到的任意矩阵都可以由原矩阵变换而来。容易观察到对角阵变换得的任意矩阵都满足「间隔为n(矩阵大小)的斜线上的元素都为1,其余元素为0」,如:
0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0 0
1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0
0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0
0 0 1 0 0 0 0 / 0 0 0 1 0 0 0 / 0 0 0 0 0 0 1
0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0
0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0
所以只要找原矩阵含'1'最多的一组间隔为n的斜线,以它作为变换后矩阵的对角线,然后计算需要对多少个元素进行取反才能使这组斜线上元素全为'1'且其余全为'0'。
【代码】
char map[2005][2005];
int CountDiag(int num, int n)//计算某一组斜线的'1'个数
{
int row = 1, col = num;
int cnt = 0;
while (col <= n)
{
cnt += map[row++][col++] - '0';
}
row = n - num + 2;
col = 1;
while (row <= n)
{
cnt += map[row++][col++] - '0';
}
return cnt;
}
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> (map[i] + 1);//使下标为1到n,方便代码书写
}
//计算原矩阵'1'的个数cnt1
int cnt1 = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= n; ++j)
{
if (map[i][j] == '1')
{
cnt1++;
}
}
}
//计算'1'最多的一组斜线的'1'个数maxc
int maxc = 0, cnt;
for (int i = 1; i <= n; ++i)
{
cnt = CountDiag(i, n);
if (cnt > maxc)
{
maxc = cnt;
}
}
//计算答案
cout << (n - maxc) + (cnt1 - maxc) << endl;
return;
}
F1&F2. Promising String
(F2的代码也可以过F1,仅数据规模不同)
【题意】给定一个只含'+', '-'的字符串,求它有多少个子串满足「将若干"--"换成"+"后'+'与'-'个数相等」
【思路】将"--"换为"+"可以使'+'和'-'的个数差变化3,所以起初我根据题意得到,判断一个子串是否满足条件的方法是:
- 计算子串中「'-'的数量」减去「'+'的数量」的差值dif
- 计算子串中"--"的个数cnt
- 若 dif ≥ 0 且 dif ≤ cnt*3 且「dif为3的倍数」则子串满足条件
但是经过观察和思考发现,在 dif ≥ 0 时 “dif ≤ cnt * 3” 这个条件是一定成立的。因为即使在最坏的情况下('+'和'-'交替分布)该条件也成立,此时若再把m个'+'换成'-',dif和cnt会同时+m,显然该条件还是成立。所以判断方法简化为:
- 计算子串中「'-'的数量」减去「'+'的数量」的差值dif
- 若 dif ≥ 0 && dif % 3 == 0 则子串满足条件
此时枚举子串再进行上述判断就能做出F1题了。做法为:先循环枚举原字符串每个元素i,再循环枚举截止i(包括i)的每个元素j,子串即为从j到i的串(闭区间[j, i])。然后用前缀和的思想记录'-'个数和'+'个数的前缀差值prefix[n],则子串 dif = prefix[i] - prefix[j-1] 。
但是这样简单的想法并不能满足F2题,因为F2题中n的规模是2e5,必须找到小于o(n^2)的解决方法。如果枚举每个子串,长度为n的字符串有n(n-1)/2个子串,就算判断只需要o(1)总时间也是o(n^2),肯定超时。所以需要在此方法的基础上继续优化。
观察枚举子串的方法,想到:我们或许可以只枚举子串的终点i,用某种方法o(1)计算以i为终点的所有子串中满足条件的子串个数。因为子串的dif由prefix相减得到,我们可以把判断的对象从dif转移到prefix。判断条件 dif % 3 == 0 可以等价为 prefix[i] % 3 == prefix[j - 1] % 3 ,由此想到可以按模3的余数将prefix分为三组;而 dif ≥ 0 可以等价为 prefix[i] ≥ prefix[j - 1] ,由此想到可以往组中每加入一个prefix[i]前就计算组中小于等于prefix[i]的prefix个数,这相当于需要「区间修改」和「区间和计算」操作,可以通过树状数组实现。
最终我们的方法变为:用树状数组维护三个「记录当前每种大小的prefix的个数」的数组,每个数组中的相邻prefix之间差值为3。遍历原数组每个元素i,每次将prefix[i]加入前计算数组中已有的值小于等于它的prefix的个数,最后答案就为每次算得的个数的总和。这样就可以o(nlogn)解决这道题了。
还有很多细节,见代码。
【代码】
int prefix[200005];
int tree[3][200005];
int lowbit(int x)
{
return x & -x;
}
void add(int dst, int v, int ord, int n)//使第ord个被维护数组dst位置加上v
{
while (dst <= n + 1)
{
tree[ord][dst] += v;
dst += lowbit(dst);
}
return;
}
long long prefix_sum(int dst, int ord)//求第ord个被维护数组截止到dst位置的前缀和
{
long long res = 0;
while (dst > 0)
{
res += tree[ord][dst];
dst -= lowbit(dst);
}
return res;
}
void solve()
{
long long ans = 0;
int n;
memset(tree, 0, sizeof(tree));
cin >> n;
string s;
cin >> s;
s = '\0' + s;//使下标变为1到n,方便代码书写
add((prefix[0] + n) / 3 + 1, 1, (prefix[0] + n) % 3, n);//prefix[0]也要考虑
for (int i = 1; i <= n; ++i)
{
prefix[i] = prefix[i - 1] + (s[i] == '-' ? 1 : -1);//计算prefix
ans += prefix_sum((prefix[i] + n) / 3 + 1, (prefix[i] + n) % 3);//计算个数
add((prefix[i] + n) / 3 + 1, 1, (prefix[i] + n) % 3, n);//加入preifx
//满足条件要求:1.prefix[i]>=prefix[j] 2.prefix[i]-prefix[j]为3的倍数
//prefix[i]-prefix[j]为3的倍数 <=> prefix[i]%3==prefix[j]%3
//用树状数组维护三个数组
//插入一个prefix[i]时,找已有的值小等它的prefix的个数
}
cout << ans << '\n';
return;
}
END
(第一次写题解也是第一次写博客 表达清楚自己的想法真不是件易事啊)