个人思路,仅作记录,可以参考,欢迎交流。
比赛地址:传送门
(不得不说,这次的题目难度和分布有点诡异)
A. Minimums and Maximums
【题意】给定两个区间A=[l1,r1],B=[l2,r2],求满足「最小值个数在A内且最大值个数在B内」的数组的最小长度
【思路】若A∩B=Ø,则最大值和最小值不能相等,数组最短时有l1个最大值,l2个最小值,答案为l1+l2;若A∩B≠Ø,则最大值可以与最小值相同,个数为A∩B左边界,答案为min(l1,l2)。
【代码】
void solve()
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if (l1 > r2 || l2 > r1)
{
cout << l1 + l2 << '\n';
}
else
{
cout << max(l1, l2) << '\n';
}
return;
}
B. Robots
【题意】给定一个二维二进制地图(矩阵),将所有为1的点看作一个整体,求能否在将它们同时进行上下左右移动后使任意一个1到达矩阵左上角且没有1越过边界
【思路】最容易到达左上角的显然是最靠近左上的1,且这样的1只能有一个——它的上方和左方的所有空间都不能有1。一种可行的解决方案为:先逐行从左向右找到第一个1(图中橙色块),此时其上方空间(完全在图中蓝色部分内)已经能保证没有1,只要再看其左方空间剩余部分(图中绿色部分)内有没有1,有1则答案为NO,没有1则答案为YES。
【代码】
void solve()
{
char map[6][6];
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
{
cin >> map[i] + 1;
}
int row = 0, col = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= m; ++j)
{
if (map[i][j] == 'R')
{
row = i;
col = j;
break;
}
}
if (row || col)
{
break;
}
}
if (row == 0 && col == 0)
{
cout << "NO\n";
return;
}
else
{
for (int i = row + 1; i <= n; ++i)
{
for (int j = 1; j < col; ++j)
{
if (map[i][j] == 'R')
{
cout << "NO\n";
return;
}
}
}
}
cout << "YES\n";
return;
}
C. Binary String
【题意】给定一个二进制串,你可以任意删除其左端或右端的元素直到其为空。求在此过程中「“剩余0的个数”和“已删除1的个数”的最大值」的最小值
【思路】分析删除的过程:设初始时串长度为 、串中0的个数为
,则删除过程中“剩余0的个数”(设为
)由
单调减为0,“已删除1的个数”(设为
)由0单调增为
。显然当两者相等时两者的最小值达到最小。令
,由
得
即
。也就是说当两者相等时一定刚好删除了
个数,所以问题转化为:求删除
个数时
的最小值。可以O(n)暴力枚举每一种删法的
,详见代码。
【代码】
void solve()
{
string s;
cin >> s;
int n = s.size();
int quota = 0;//原串中0的个数
for (int i = 0; i < n; ++i)
{
if (s[i] == '0')
{
quota++;
}
}
int cnt0 = 0;//对应题解中delete0
for (int i = 0; i < quota; ++i)//先算全删在左边能删掉的0个数
{
if (s[i] == '0')
{
cnt0++;
}
}
int ans = quota - cnt0;
for (int i = 1; i <= quota; ++i)//由「全删左」到「全删右」,枚举所有的删除方法
{
if (s[quota - i] == '0')//左边少删一个
{
cnt0--;
}
if (s[n - i] == '0')//右边多删一个
{
cnt0++;
}
ans = min(ans, quota - cnt0);//更新答案
}
cout << ans << '\n';
return;
}
D. Dog Walking
【题意】在一条数轴上有一点,初始时在0处。给定数组a为该点n秒中部分秒数的位移,其余秒数的位移可能为[-k,k]内任意整数,但该点最后必须返回0点。已知n和k,求该点在n秒内在数轴上最多能经过的整数点的数量。
【思路】求经过的整数点数量最大值其实就是求该点全程路径的最右端到路径最左端的距离(加1),但是全程到达最左端和最右端的时间没法确定,一切皆有可能。所以可以考虑枚举每两个时间点,算所在位置的差的最大可能值。
设数组a的总和为sum(a),则未知位移的秒数的位移总和要为-sum(a),但是总和固定的情况下每个位移既可以为正又可以为负,这有点难讨论。有一个很妙的想法——可以将问题等效转化为:每个未知位移的时间点的位移都为-k,即每秒的基本位移确定,设为数组b,而在这些特定时间点可以额外增加[0,2k]的位移,且最终额外位移的总和要为固定值-sum(b)。这样的问题转化让我们只要在非负数范围内研究问题,简化了思考。
不难想到一种做法:枚举每两个时间点,算出两个时间点间额外位移可能取值的范围,考虑以下两种情况左右端的最大距离:
- 前时间点作为右端,后时间点作为左端
- 前时间点作为左端,后时间点作为右端
详见代码(虽然可能看不懂,但是用文字也不知道怎么解释了)。
【代码】
long long a[3005];//问题转化前每个时间点的位移
long long b[3005];//问题转化后每个时间点的位移
long long sum[3005] = { 0 };//位移的前缀和,即每个时间点的位置
long long cnt[3005] = { 0 };//可额外位移次数的前缀和
long long Range(long long num, long long lef, long long rig)//num不能超过[lef,rig]范围,否则取边界值
{
if (num > rig)
{
return rig;
}
else if (num < lef)
{
return lef;
}
else
{
return num;
}
}
void solve()
{
long long n, k;
cin >> n >> k;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
b[i] = (a[i] ? a[i] : -k);
sum[i] = sum[i - 1] + b[i];
cnt[i] = cnt[i - 1] + (a[i] == 0);
}
k *= 2;//问题转化,k变化
if (sum[n] > 0 || -sum[n] > k * cnt[n])
{
cout << -1 << '\n';
return;
}
//每处可以右移[0,k],一共要右移-sum[n]
long long ans = 0;
for (int i = 0; i <= n; ++i)
{
for (int j = i; j <= n; ++j)
{
long long lef = -sum[n] - (cnt[n] - cnt[j] + cnt[i]) * k;
long long rig = -sum[n];
long long move = (cnt[j] - cnt[i]) * k;
ans = max(ans, sum[i] - sum[j] + 1 - max(lef, 0ll));//尽量少右移
ans = max(ans, sum[j] - sum[i] + 1 + Range(move, lef, rig));//尽量多右移
}
}
cout << ans << '\n';
return;
}
E. Moving Chips
(谜之简单的E题)
【题意】给定一个2*n的二进制地图(矩阵),可以任意控制任一个1移动并吃掉其他的1,问最少经过多少次移动可以使地图上只剩一个1
【思路】问题可以转化为:将所有为1的点用水平或竖直的线连成一个连通图至少需要多长的线(如图)。虽然这个转化好像没什么用,或许思考起来可以更直观一点吧。
想贪心之类的,策略分析起来似乎有点复杂,还是动态规划吧。想法是控制最左的一个1不断向右移动,若遇到同一横坐标的另一行有1,可以选择纵向移动到另一行或让另一行的1纵向移到自己的位置代替自己。设dp[i][j]为考虑完前i列且处在第j行(j=0代表第一行,j=1代表第二行)时已经移动的距离。
初始时,位于第一个有1的列:
- 若两行都为1,从哪个1出发都要另一个1移动过来,dp初值均为1;
- 若一行为1一行为0,从0出发需要1移动一步,从1出发不需要移动,dp初值分别为1和0。
经过每一列考虑四种情况:
- 两行都为1
- 第一行为1第二行为0
- 第一行为0第二行为1
- 两行都为0
但其实都只有两种走法:
- 直行,若另一行有1自己过来
- 转到另一行并继续直行
递推完后,答案即为考虑完最后一个有1的列时dp两个值中的最小值。具体见代码。
【代码】
char map[3][200005];
int dp[200005][2];
//这里有点乱,解释一下:
//map[1][i]代表第一行,map[2][i]代表第二行
//dp[i][0]代表第一行,dp[i][1]代表第二行
//dp可以用「滚动数组」优化一下空间,但我懒
void solve()
{
memset(dp, 0x3f, sizeof(dp));
int n;
cin >> n;
cin >> map[1] + 1;
cin >> map[2] + 1;
int start = 0, end = 0;
for (int i = 1; i <= n; ++i)
{
if (map[1][i] == '*' || map[2][i] == '*')
{
start = i;
break;
}
}
for (int i = n; i >= 1; --i)
{
if (map[1][i] == '*' || map[2][i] == '*')
{
end = i;
break;
}
}
dp[start][0] = (map[1][start] == '.');
dp[start][1] = (map[2][start] == '.');
if (map[1][start] == '*' && map[2][start] == '*')
{
dp[start][0] = 1;
dp[start][1] = 1;
}
for (int i = start + 1; i <= end; ++i)
{
if (map[1][i] == '*' && map[2][i] == '*')
{
dp[i][0] = min(dp[i - 1][0], dp[i - 1][1]) + 2;
dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]) + 2;
}
else if (map[1][i] == '*')
{
dp[i][0] = min(dp[i - 1][0] + 1, dp[i - 1][1] + 2);
dp[i][1] = min(dp[i - 1][1], dp[i - 1][0]) + 2;
}
else if (map[2][i] == '*')
{
dp[i][0] = min(dp[i - 1][1], dp[i - 1][0]) + 2;
dp[i][1] = min(dp[i - 1][0] + 2, dp[i - 1][1] + 1);
}
else
{
dp[i][0] = min(dp[i - 1][0] + 1, dp[i - 1][1] + 2);
dp[i][1] = min(dp[i - 1][1] + 1, dp[i - 1][0] + 2);
}
}
cout << min(dp[end][1], dp[end][0]) << '\n';
return;
}
(上大分喽,芜湖!)