题意
给出一个长度为 n 的 01 串,现在规定一个串如果相邻两个 1 的位置相隔为 k ,那么这个串就是好串,现在你可以将某个位置的字符翻转,问最少需要多少次可以把这个串变成一个好串?
思路
本来是练习DP的,但是想着想着跑偏了。
好串格式应该是0000001000100010001000000
前面全都是0,中间出现周期,最后也全是0,我们只要求出一个区间[l,r],这个区间的开头和结尾都是 1 ,并且合法即可。
那么我们就可以枚举区间的开头,只要可以O(logn)的求出当前开头以哪个下标结尾最小就可以。
但是想了很久想不到。
看了题解有两种解法:DP和算是思维吧。
DP
dp[i][0]
表示前 i 位合法,并且第 i 位为 0
dp[i][1]
表示前 i 位合法,并且第 i 位为 1
转移方程如下:
当前 i - 1 项合法时,第 i 项为 0 ,那么前 i 项必定合法
当第 i 项为 1 时,如果要合法,那么前 i-k 项肯定要合法并且第 i-k 项为 1 ,而且((i-k,i))全都是0 ,或者第 i 项为第一个 1 ,代价就是把前 i-1 项中的 1 全部变为0。
代码
#include<bits/stdc++.h>
#define pb push_back
const int N=1e6+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
char str[N];
int dp[N][2],pre[N];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n,m;
scanf("%d%d%s",&n,&m,str+1);
for(int i=1; i<=n; i++)
{
pre[i]=pre[i-1]+(str[i]=='1');
dp[i][0]=min(dp[i-1][0],dp[i-1][1])+(str[i]=='1');
dp[i][1]=pre[i-1]+(str[i]=='0');
if(i-m>0)
dp[i][1]=min(dp[i][1],dp[i-m][1]+pre[i-1]-pre[i-m]+(str[i]=='0'));
}
printf("%d
",min(dp[n][0],dp[n][1]));
}
return 0;
}
思维
我们可以知道好串的两段都有若干个 0,中间部分是周期性的串。
所以我们先可以把所有的 1 变成 0,然后在中间找一个子串使其成周期性,并修改总代价。
每个周期只有一个 1 ,而且他们的位置随着第一个 1 的位置固定而固定,所以我们可以枚举 1 的下标。
每次枚举的时候定义一个变量 cnt ,表示本次要减少的代价
当 (str[i]=='1') 时,那么本来这个位置不用改变,所以我们要减去把它变为 0 的代价,否则 这个位置应该变成 1 ,加上变为 1 的代价。
每更新一个周期就更新 cnt 和 ans ,其实这样有点类似于最大子段和。
cnt == 0 的时候就是前面遍历过的周期都维持 0 。
(其实这个思想在一道树形DP题中遇到过,当时还是看的kuangbin的博客)
代码
#include<bits/stdc++.h>
#define pb push_back
const int N=1e6+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
char str[N];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n,m;
scanf("%d%d%s",&n,&m,str+1);
int sum=0,ans=inf;
for(int i=1;i<=n;i++)
sum+=(str[i]=='1');
for(int i=1;i<=m;i++)
{
int cnt=0;
for(int j=i;j<=n;j+=m)
{
if(str[j]=='1') cnt--;
else cnt++;
cnt=min(cnt,0);
ans=min(sum+cnt,ans);
}
}
printf("%d
",ans);
}
return 0;
}