比赛链接:https://vjudge.net/contest/405905#problem/D
题意:
给你一个长度为n的由0或1构成的串s,你需要切割这个串,要求切割之后的每一个子串长度要小于等于k。且每一个子串内不能全都是01交替,就比如
00101010、11010101这样没有问题,不需要切割,因为前面有两个相同的
01010101、10101010这样就不行,就必须切割开
问你最少需要切割多少次
题解:
我们设定输入的01串下标从1开始
我们使用dp[i]表示:s字符串的从第1个字符到第i个字符最少需要切割多少次
dp转移方程dp[i+j]=min(dp[i+j],dp[i-1]+1)
可以说我们使用dp[i-1]的值去维护后面dp的值,要保证[i,i+j]这一个子串不需要切割,且长度小于等于k
复杂度也就是O(n*k),对于第一题是没问题的
AC代码:
1 #include <map> 2 #include <set> 3 #include <list> 4 #include <queue> 5 #include <deque> 6 #include <cmath> 7 #include <stack> 8 #include <vector> 9 #include <bitset> 10 #include <cstdio> 11 #include <string> 12 #include <cstdlib> 13 #include <cstring> 14 #include <iostream> 15 #include <algorithm> 16 using namespace std; 17 typedef long long ll; 18 typedef unsigned long long ull; 19 const ll mod = 1e9 + 7; 20 const int maxn = 1e3 + 10; 21 const int INF = 0x3f3f3f3f; 22 char s[maxn]; 23 int dp[maxn]; 24 int main() 25 { 26 int t; 27 scanf("%d", &t); 28 while (t--) 29 { 30 int n, k; 31 memset(dp, INF, sizeof(dp)); 32 dp[0] = 0; 33 scanf("%d%d", &n, &k); 34 scanf("%s", s + 1); 35 for (int i = 1; i <= n; ++i) 36 { 37 int last = 0, flag = 0, now = s[i] - '0', nnn = 0; 38 while (1) 39 { 40 if (flag == 0 || nnn == 1) 41 { 42 dp[i + last] = min(dp[i - 1] + 1, dp[i + last]); 43 } 44 last++; 45 if (last == k || i + last > n) 46 break; 47 if (nnn) 48 continue; 49 if (s[i + last] - '0' == now) 50 { 51 nnn = 1; 52 } 53 else 54 { 55 flag = 1; 56 now = 1 - now; 57 } 58 } 59 //if(i==1) printf("%d*** ",last); 60 } 61 printf("%d ", dp[n] - 1); 62 } 63 return 0; 64 } 65 /* 66 4 67 6 3 68 111000 69 5 2 70 11010 71 3 3 72 110 73 3 3 74 101 75 76 1 77 4 4 78 1110 79 */
题解2(线段树+dp):
但是我上面的dp方程无法优化,因为你要使用dp[i-1]的值去更新dp[i+j]的值,那么最坏情况也就是对于j(1<=j<=k),dp[i-1]可以更新每一个dp[i+j]
那么最坏复杂度就是O(n*k)是无法优化的,所以要另辟蹊径
那我们可以把dp转移方程改成
dp[i]=min(dp[i-j]+1,dp[i])
可以说反转了一下,我们需要保证下标为[i-j+1,i]的子串不需要分割且长度小于等于k
那么我们可以使用线段树来维护所有dp[i]的值
对于dp[i]我们可以在线段树中查找区间[i-k,i-1]中的最小值就可以,但是因为题目要求分割后的子串中不能全部是01交替
所以我们查找区间[i-k,i-1]的最小值,我们需要找到以i为结尾,向左边找交替出现01的长度pre,就比如下面的串
0101101(下标从1开始)
i=7的话,如果k无限大,那么dp[i]就可以由dp[3]、dp[2]、dp[1]得到,因为下标4、5位置都是1,那么就说明如果对于变量j<4,那么子串[j+1,i]就可以满足题目要求
然后使用线段树找出来区间[i-k,i-pre]中的最小值就可以了
大致意思理解就可以,细节之处可以自己改一下,毕竟每一个人写的方式不一样
AC代码:
1 #include <map> 2 #include <set> 3 #include <list> 4 #include <queue> 5 #include <deque> 6 #include <cmath> 7 #include <stack> 8 #include <vector> 9 #include <bitset> 10 #include <cstdio> 11 #include <string> 12 #include <cstdlib> 13 #include <cstring> 14 #include <iostream> 15 #include <algorithm> 16 #define lson rt<<1,L,mid 17 #define rson rt<<1|1,mid+1,R 18 #define mem(a,b) memset(a,b,sizeof(a)) 19 using namespace std; 20 typedef long long ll; 21 typedef unsigned long long ull; 22 const ll mod = 1e9 + 7; 23 const int maxn = 1e5 + 10; 24 const int INF = 0x3f3f3f3f; 25 int root[maxn<<2],dp[maxn]; 26 char s[maxn]; 27 void push_up(int rt) 28 { 29 root[rt]=min(root[rt<<1],root[rt<<1|1]); 30 } 31 void update(int rt,int L,int R,int pos,int val) 32 { 33 if(L==R) 34 { 35 root[rt]=val; 36 return; 37 } 38 int mid=(L+R)>>1; 39 if(pos<=mid) 40 update(lson,pos,val); 41 else update(rson,pos,val); 42 push_up(rt); 43 } 44 int query(int rt,int L,int R,int LL,int RR) 45 { 46 if(LL<=L && RR>=R) 47 { 48 return root[rt]; 49 } 50 int mid=(L+R)>>1,ans=INF; 51 if(LL<=mid) ans=min(ans,query(lson,LL,RR)); 52 if(RR>mid) ans=min(ans,query(rson,LL,RR)); 53 return ans; 54 } 55 int main() 56 { 57 int t; 58 //update(1,1,2,1,0); 59 //printf("%d ",query(1,1,2,1,1)); 60 scanf("%d",&t); 61 while(t--) 62 { 63 int n,k; 64 //mem(root,INF); 65 memset(root,INF,sizeof(root)); 66 scanf("%d%d",&n,&k); 67 update(1,1,n+1,1,0); 68 update(1,1,n+1,2,1); 69 dp[1]=0; 70 dp[2]=1; 71 //printf("%d***** ",query(1,1,n+1,2,2)); 72 scanf("%s",s+2); 73 74 int pre=1; 75 for(int i=3;i<=n+1;++i) 76 { 77 if(s[i]==s[i-1]) 78 { 79 pre=1; 80 dp[i]=query(1,1,n+1,max(i-k,1),i-1)+1; 81 //printf("%d ",dp[i]); 82 } 83 else 84 { 85 pre++; 86 if(pre>=k || pre==i-1) 87 { 88 dp[i]=query(1,1,n+1,i-1,i-1)+1; 89 //printf("%d* ",dp[i]); 90 } 91 else 92 { 93 dp[i]=query(1,1,n+1,max(i-k,1),i-pre-1)+1; 94 //printf("%d*** ",dp[i]); 95 } 96 }//printf("** +1"); 97 update(1,1,n+1,i,dp[i]); 98 // 99 } 100 //printf("*** "); 101 printf("%d ",dp[n+1]-1); 102 } 103 return 0; 104 }
题解3(双端队列维护):
我们维护一个单调递增的队列,对于一个位置i,我们需要找到以i为结尾,向左边找交替出现01的长度pre
然后如果pre>k或者pre==i(pre==i表示串从头到i位置都是01交替)
那么dp[i]=dp[i-1]+1;
否则就从队列头部取出元素+1就是dp[i]的值
给你四个变量i、j、kk、l(i<j<kk<l,l-i<=k)
现在我们求dp[l]的值,如果dp[j]可以用来维护dp[l](意味这子串[j+1,i]可以当成一个切割后的子串)。如果dp[kk]<dp[j]导致dp[j]被移出队列
那么也就意味着dp[j]无法维护dp[l]的值,这可以吗?
其实是可以的,因为如果dp[j]可以维护dp[l]那么dp[i]也是可以
且如果dp[i]在队列中,dp[i]<dp[j],那么dp[j]丢失了也就没有关系了
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <queue> #include <map> using namespace std; typedef long long ll; const int maxn=1e5+10; const int INF=0x3f3f3f3f; char s[maxn]; int dp[maxn],que[maxn]; int main() { int t; scanf("%d",&t); while(t--) { int n,k,start=0,last=0; //memset(dp,INF,sizeof(dp)); scanf("%d%d",&n,&k); scanf("%s",s+1); dp[0]=0; que[++last]=0; //que[++last]=1; int pre=1; for(int i=1; i<=n; ++i) { if(start<last && i-que[start+1]>k) { //printf("%d %d*** ",que[start+1],i); start++; } if(i==1 || s[i]==s[i-1]) { pre=1; dp[i]=dp[que[start+1]]+1; //if(i==3) printf("%d %d ",que[start+1],start); } else { pre++; if(pre==i || pre>=i-que[start+1]) { dp[i]=dp[que[last]]+1; //if(i==n) } else { dp[i]=dp[que[start+1]]+1; } } while(start<last && dp[i]<dp[que[last]]) last--; que[++last]=i; //printf("%d ",dp[i]); } //printf(" "); printf("%d ",dp[n]-1); } return 0; } /* 4 6 3 111000 5 2 11010 3 3 110 3 3 101 */