题目大意
给你一个长度为 (n) 的 (01) 字符串,要求让这个字符串的每个 (1) 字符之间的距离恰好都为 (k) ,问至少要修改几个字符。
思路
显然这是道 dp 题。
-
(dp_{i ,0}) 表示到第 (i) 个字符为止,只让第 (i) 位为 (1),其余 (i-1) 位均位 (0) 时需要的操作次数。
-
(dp_{i,1}) 表示到第 (i) 个字符为止,让第 (i) 位为 (1),且保证在这之前合法所需要的最少操作次数。
那显然,只要维护一个前缀和 (sum_i) ,统计到第 (i-1) 个字符位置 (1) 的数量,那么:
[dp_{i,0}=egin{cases}sum_{i-1} (s_i=1)\sum_{i-1}+1 (s_i=0)end{cases}
]
Ps: (s_i) 表示字符串的第 (i) 位所表示的字符。
同时,我们也很容易推出另外一个转移式:
[mathop{dp_{i,1}}limits_{i-kge1}=egin{cases}min(dp_{i-k,1},dp_{i-k,0}) + sum_{i-1}-sum_{i-k} (s_i=1)\min(dp_{i-k,1},dp_{i-k,0}) + sum_{i-1}-sum_{i-k}+1 (s_i=0)end{cases}
]
这很好理解,只要保证第 (i-k) 个位置为 (1) 并将 ((i-k+1)sim (i-1)) 中的所有 (1) 都设为 (0) 即可,这步同样可以用前缀和维护。
最后计算答案的时候,将 (dp) 数组从后往前扫一遍,每次在 (dp_{i,0};,;dp_{i,1}) 中取 (min) 。在扫的同时维护一个 (num) 表示从 ((i+1) sim n) 这段中 (1) 的数量,统计答案的时候要加上这个 (num),意味着吧 ((i+1) sim n) 中所有的 (1) 设为 (0),防止出现不合法的情况。
时间复杂度:( ext{O}(n))
Code
#include<bits/stdc++.h>
using namespace std;
int read()
{
int ans=0,f=1;
char c=getchar();
while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){ans=ans*10+c-'0';c=getchar();}
return ans*f;
}
const int N=1e6+5,inf=0x7f7f7f7f;
int t,n,k,dp[N][2],sum[N],ans;
char s[N];
int main()
{
memset(dp,0x7f,sizeof(dp));
// 将 dp 数组初始化,将所有可能先设为不合法
t=read();
while(t--)
{
n=read();k=read();
scanf("%s",s+1);
for(int i=1;i<=n;++i)
// 统计前缀和
sum[i]=sum[i-1]+(s[i]-'0');
ans=sum[n]; // 这里的意思是把答案初始化为:把所有的 1 都变成 0 所需要的操作次数
for(int i=1;i<=n;++i)
{
dp[i][0]=sum[i-1];
// dp[i][0] 的转移
if(i-k>=1)
dp[i][1]=min(dp[i-k][1]+sum[i-1]-sum[i-k],dp[i-k][0]+sum[i-1]-sum[i-k]);
// dp[i][1] 的转移
if(s[i]=='0')
{
if(dp[i][0]!=inf)
dp[i][0]++;
if(dp[i][1]!=inf)
dp[i][1]++;
}
}
int num=0;
// 统计答案的时候维护一个 num,防止出现不合法情况
for(int i=n;i>=1;--i)
{
ans=min(ans,min(dp[i][1]+num,dp[i][0]+num));
if(s[i]=='1')
num++;
}
for(int i=1;i<=n;++i)
// 这是这题的一个坑点,直接 memset 会 T,必须手动清空
{
sum[i]=0;
dp[i][0]=inf;
dp[i][1]=inf;
}
printf("%d
",ans);
}
return 0;
}