寒假营第五场
B 思维
可以删任意个数的字符串,但是不能删连续两个,每种删除方案的得分是"mygo"出现的次数,问全部方案的总分。
首先一个重要的观察是,由于不能删连续字符,删除后能出现"mygo"的字符串实际上只有八种,具体来说就是"mygo"中间的三个空都可以插入一个其他字符。对于这八种字符串,都有唯一的删除方式使删完出现一个"mygo"。
另一个重要的观察是,既然遍历所有删除方式,分别计算贡献很麻烦,那么我们可以分开计算每一个"mygo"对答案的贡献。对于每一个"mygo",得到他的删除方式再mygo内部是唯一的,而外部可以随意删。我们需要外部随意删有多少种方案,这些方案的贡献都是1,也就是只含这一个mygo。
这就又引出一个问题,就是外部随意删的方案数怎么算。注意到这是随意删,因此方案数和具体是什么字符串无关,只和字符串长度有关,因此我们可以预处理,不用每次都算。具体来说,我们需要预处理出f[i]
,表示长度为i的字符串,不删连续字符的前提下有多少种删除方案。这可以用一个递推实现,如果第i个字符被删了,那么第i-1个就不能删,再往前的随意,这样的方案数等于f[i-2]
,如果第i个没被删,那么前i-1个都随意,这样的方案数是f[i-1]
。最终得到f[i]=f[i-1]+f[i-2]
,初始条件是f[1]=1,f[2]=2
。这实际上就是斐波那契数列的递推式。
剩下的就是遍历所有mygo,分别计算贡献。遍历所有mygo具体可以遍历遍历整个字符串,检查每个位置是否出现上面八种串。
using namespace std;
const int M=1e9+7;
const int N=2e5+10;
int f[N];
string s;
bool check(string a,string b){
for(int i=0;i<a.size();i++){
if(a[i]==' ')continue;
if(a[i]!=b[i])return 0;
}
return 1;
}
int main(){
cin>>s;
int n=s.size();
s="!"+s+"soyorinlove";
f[0]=1;
f[1]=2;
for(int i=2;i<=n;i++){
f[i]=(f[i-1]+f[i-2])%M;//预处理
}
vector<string> t={"mygo","m ygo","m y go","m y g o",
"my go","my g o","myg o","m yg o"};
int ans=0;
// for(int i=0;i<=n;i++)cout<<f[i]<<' ';
// cout<<'\n';
for(int i=1;i<=n;i++){
for(auto j:t){
string x=s.substr(i,j.size());//检查是否出现八种字符串
if(check(j,x)){
ans=(ans+1ll*f[i-1]*f[n-(i+j.size())+1]%M)%M;//计算贡献
}
}
}
cout<<ans;
}
C思维
关键是意识到0放在哪最大数量都是一样的,那么为了方便计算肯定是用0把所有数隔开。接下来就好分析了,每一个数两侧能放的0的总数是a[i]-1
,那么就从左往右遍历每一个能放零的空,每次确定0个数后,相当于把前后两个非零数的0的额度用了一部分,因此可以把这些额度剪掉,然后每个空能放的0的个数是前后两个非零数剩余额度的最小值。
边界情况是第一个数左边和最后一个数右边的空,只受一个数的额度约束。
using namespace std;
const int N=1e5+10;
int a[N];
int main(){
int n;cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
long long ans=0;
ans+=a[1]-1;//最左边的空只收第一个数约束
a[1]=1;
for(int i=2;i<=n;i++){
int t=min(a[i-1],a[i]);//确定0个数
t--;
a[i]-=t;//额度减去本次放的0
a[i-1]-=t;
ans+=t;
}
ans+=a[n]-1;//最右边的空只收最后一个数限制
cout<<ans;
}
E 思维
每次可以选一个偶数K,然后给前k个数8加上一个[1,k],问能否将数组变为不减的。由于加上的是一个升序数组[1,k],每加一次都会让所有a[i]-a[i-1]变大1,因此不限操作次数的话早晚能让可以被操作的数组变成升序的。因此只要初始序列种所有元素都能被操作,就一定能变成升序的。
每次能操作的都是长度为偶的序列,因此如果序列长度为偶就一定能变成升序的。如果长度为奇数,前n-1个数能被操作,要满足第n-1个数小于等于第n个数,那么第n-1个数的大小就有一个上界,往前同理,第n-2,n-3个数的大小也都有上界。我们可以一路推出前面所有数的上界。因为操作次数越多越可能变成升序的,那么我们一定会把前面所有数都操作到上界,那么如果上界是不减的,最终就可以变成不减的。
具体往前推上界的时候,大区间操作会影响小区间,因此操作次数是会继承后一个数的。
using namespace std;
const int N=1e5+10;
long long a[N];
int main(){
int T;
cin>>T;
while(T--){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
if(n%2==0){
cout<<"YES";
}
else{
long long cnt=0;
bool ok=1;
for(int i=n-1;i>=2;i-=2){
//从倒数第二个数开始,最后一个操作不了
a[i]+=cnt*i;
a[i-1]+=cnt*(i-1);
//大区间的操作会影响小区间,先加上
if(a[i]>a[i+1]){
ok=0;
break;
//如果此时已经降序了,就失败了
//因为接下来的操作只能让小区间内数更大
}
long long t=(a[i+1]-a[i])/i;//计算到上界需要几次操作
a[i]+=t*i;//操作到上界
a[i-1]+=t*(i-1);
cnt+=t;//累加操作次数
}
for(int i=1;i<n;i++){
if(a[i]>a[i+1]){//检查是否不减
ok=0;
break;
}
}
if(ok)cout<<"YES";
else cout<<"NO";
}
cout<<'\n';
}
}
F 思维
这问还要输出操作次数。用E的写法就有点麻烦了。所以换了一种更简单的写法。
每次操作由于加上的是一个公差为1的等差数列,实际效果其实就是让前后两个数的差距缩小1(如果前面比后面大的话)。那么所需的最大操作次数实际上就是相邻两个数差距的最大值。
判断能否变为不减还可以用类似E的方法,这部分和计算最小操作次数是可以分开的。
using namespace std;
const int N=1e5+10;
long long a[N];
int main(){
int t;
cin>>t;
while(t--){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
long long ans=0,add=0;
for(int i=n-1;i>=1;i--){
if(n%2){
if(a[i+1]+add<a[i]){
cout<<-1;
goto over;
}
else{
if(i%2==0){
add+=(a[i+1]+add-a[i])/i;
//add就是操作次数
//其实并不需要真的算出来每个数的上界并修改
}
}
}
ans=max(ans,a[i]-a[i+1]);//更新最小次数
}
cout<<ans;
over:cout<<'\n';
}
}
GH 思维
构造一个排列p,让pi+i为质数。这题的教训是多试几组例子,样例不够就自己造几组,一般就能找到规律了。
比如对于1234,如果我给他加上4321,就变成了5555,都是质数,且用的也是一个排列。这给我们的启示是可以去寻找一段,然后把排列中连续的一段倒着加上,就能形成一段质数。为此我们可以先把所有不超过2n的质数都筛出来,然后对于一段,我们去寻找最小的可行的质数,作为加完之后的和,这样一段段处理就行了。
using namespace std;
bool vis[2020];
int ans[1010];
int main(){
int n;
cin>>n;
//质数筛
vis[1]=vis[0]=1;
for(int i=2;i<=sqrt(2*n);i++)
if(vis[i]==0)//如果是质数
for(int j=i*2;j<=n*2;j+=i)//不超过n的所有倍数都标记为合数
vis[j]=1;
for(int i=n;i>=1;i--){
int x=i+1;//从i+1开始找
while(vis[x])x++;//找最小的质数作为和
// cout<<i<<' '<<x<<'\n';
for(int j=x-i;j<=i;j++){//把前面一段都倒着加上一段排列
ans[j]=x-j;
}
i=x-i;//跳到这段前面一点,不减1是因为for结束时会减1
}
for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
}
J dp
n个区间,每个区间内选一个数,使得整个数组相邻数绝对值之差的和最小。
如果区间有交集,那么选交集里任意一个数都可以使得相邻数绝对值之差为0。但可能不是全部区间都有交集,对于有交集的可以按上述方法,对于没交集的,想要离得最近,元素肯定都在端点上,可以dp处理,dp(i)(j)表示第i个数取第i个区间左/右端点时,前i个区间的相邻数绝对值之差之和的最小值,j取0/1分别表示第i个数取左右端点。
using namespace std;
const int N=1e5+10;
int l[N],r[N];
vector<pair<int,int>>v;
long long dp[N][2];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>l[i]>>r[i];
int L=1,R=1e9;
for(int i=1;i<=n;i++){//先处理出来区间的交集关系
if(l[i]>R||r[i]<L){//如果没有交集
v.push_back({L,R});//上一个区间成为一个独立区间
L=l[i];//更新当前区间
R=r[i];
}
L=max(L,l[i]);
R=min(R,r[i]);//如果有交集,当前区间取交集
}
v.push_back({L,R});//别忘了把最后一个区间也放进来
//至此处理出了所有独立的交集
for(int i=1;i<v.size();i++){
//当前区间取左端点,前一个区间可以取左右端点
dp[i][0]=min(dp[i-1][0]+abs(v[i].first-v[i-1].first),dp[i-1][1]+abs(v[i].first-v[i-1].second));
//当前区间取右端点,前一个可以取做右端点
dp[i][1]=min(dp[i-1][0]+abs(v[i].second-v[i-1].first),dp[i-1][1]+abs(v[i-1].second-v[i].second));
}
cout<<min(dp[v.size()-1][1],dp[v.size()-1][0]);
}
K 完全背包
每个人通知b个人的代价是a,只限制通知人数,不限制具体通知谁。问通知全部n个人的最小代价。这其实就是一个完全背包,人数n是容量,代价是价值,只不过这个价值我们想要他最小化,那就把完全背包里的max换成min。
另一点区别是,至少要有一个人是直接通知的,那么我们算出dp[1-n]后,再用dp结果算一下i个人直接通知,剩下人相互通知的最小代价。这个才是最终的最小代价
using namespace std;
const int N=1e3+10;
long long dp[N];
int main(){
int n,p;
cin>>n>>p;
for(int i=1;i<=n;i++)dp[i]=1e9;
dp[0]=0;
for(int i=1;i<=n;i++){
int a,b;
cin>>a>>b;
if(b>=n)b=n-1;//能通知大于n个人,相当于能通知n个人
//有一个人必须直接通知,因此最大是n-1
for(int j=0;j<=n;j++){//完全背包容量从小到大枚举
dp[j]=min(dp[j],dp[max(0,j-b)]+a);
}
}
long long ans=1e9;
for(int i=0;i<n;i++){//一些人直接通知,剩下人相互通知,
//至少有一个人直接通知,枚举容量只能到n-1
ans=min(ans,dp[i]+(n-i)*p);
}
cout<<ans;
}