密钥WA40求调

题面

1. 密钥

题目ID:20443

最新提交:

Wrong Answer 40 分

历史最高:

Wrong Answer 40 分

时间限制: 1000ms
空间限制: 524288kB
输入文件名: key.in
输出文件名: key.out

题目描述

在一个大型数据加密系统中,当前的密钥序列 a 已经老化,需要更新为一个新的安全密钥序列 b。为了确保数据的安全性,工程师们必须逐步将密钥序列 a 变换为目标密钥序列 b。密钥序列仅包含0和1。

每次变换时,工程师可以修改密钥序列中的某个字符,将其从0变成1或者从1变成0。但由于系统老化,工程师需同时维护密钥中剩余的字符。如果变换后第ii位的字符为1,那么它会给这次变换带来 c_i ​的维护成本。因此可以认为每一次变换的维护成本为 \Sigma_{i\in S}c_i ,其中S为所有变换后为1的位置。

工程师们想要知道将密钥从 a 变成 b 的最小维护成本。

输入格式

从文件key.in 中读入数据。
数据第一行包含一个整数n,表示密钥的长度。
接下里的一行包含n个正整数,表示第i个位置为1时的维护成本 c_i ​。
第三行包含一个长度为n的字符串a,保证仅包含1和0。
第四行包含一个长度为n的字符串b,保证仅包含1和0。

输出格式

输出文件导key.out中。
输出一行包含一个整数表示最小维护成本。

样例

Input 1

5 8
19 6 11 17
01011
11000

Output 1

74

Input 2

7 6
20 19 12 10 10 15
1101010
0110101

Output 2

193

对于所有数据,保证 n\leq5×10^3,c_i\leq10^9

思路

若两个串中都为0,则无需对该位做任何操作;
若由1–>0,0–>1则都只需最多做一次操作;
其中,显然是先让权值大的1–>0先操作,
将1–>0的操作完后,再让权值小的0–>1的操作。

看完上面这些是不是感觉很简单?
那开始写吧————WA20

这是因为当两个串都为1时
有两种可能:

1.不动

显然这是一种可行的方案
如:
111111110–>111111111

2.1-->0-->1

样例中两个也都用到了该方案
以样例1为例:
01011–>00011–>00010–>00000–>10000–>11000

那么现在最重要的问题就是:
遇到(1,1)的情况时,什么时候用1方案,什么时候用2方案

代码
#include<bits/stdc++.h>//头文件
#define L long long//定义long long
using namespace std;//命名空间
struct Key{//结构体
    L id,val;//定义id:编号,和val:权值
}on[5005],of[5005];//定义on:从0变1,和of:从1变0
string s1,s2;//定义s1和s2
L n,cnt_on,cnt_of,ans,sum,cnt,c[5005];//定义n:灯的数量(将0,1串看为灯的亮与灭),cnt_on:从0变1的数量,cnt_of:从1变0的数量,ans:答案,sum:当前权值和,cnt:当前灯的数量,c:灯的权值
bool vis[5005],flag[5005];//定义vis:灯是否被按下过
bool cmp1(Key a,Key b){return a.val>b.val;}//定义cmp1:从大到小排序
bool cmp2(Key a,Key b){return a.val<b.val;}//定义cmp2:从小到大排序
bool check(int x){//判断在1,1情况下是否可以按下
    sum=0;cnt=0;//初始化
    for(int i=x+1;i<cnt_of;i++){//从x+1开始
        sum+=of[i].val;//累加权值
        cnt++;//累加灯的数量
    }
    for(int i=0;i<cnt_on;i++){//从0开始
        if(on[i].id==of[x].id)break;//如果当前灯的编号等于x的编号,跳出循环
        else{//否则
            cnt++;//累加灯的数量
            sum+=on[i].val;//累加权值
        }
    }
    if(sum<((cnt-1)*of[x].val))return 1;//如果权值和小于当前灯的权值,返回正确
    else return 0;//否则返回错误

}
void OFF(){//从1变0
    for(int i=0;i<cnt_of;i++){//从0开始
        if(vis[of[i].id]){//在1,1情况下
            if(check(i)){//判断是否可以按下
                flag[of[i].id]=1;//标记当前灯被按下过
                s1[of[i].id]='0';//将当前灯的编号变为0
                for(int j=0;j<n;j++)if(s1[j]=='1')ans+=c[j];//累加权值
            }else continue;//否则继续
        }else{//否则
            s1[of[i].id]='0';//将当前灯的编号变为0
            for(int j=0;j<n;j++)if(s1[j]=='1')ans+=c[j];//累加权值
        }
    }
    return ;
}
void ON(){//从0变1
    for(int i=0;i<cnt_on;i++){//从0开始
        if(s1[on[i].id]=='1')continue;//如果当前灯的编号为1,继续
        s1[on[i].id]='1';//将当前灯的编号变为1
        for(int j=0;j<n;j++)if(s1[j]=='1')ans+=c[j];//累加权值
    }
    return ;
}
int main(){
    freopen("key.in","r",stdin);
    freopen("key.out","w",stdout);
    scanf("%lld",&n);//输入n
    for(int i=0;i<n;i++)scanf("%lld",&c[i]);//输入c
    cin>>s1>>s2;//输入s1和s2
    for(int i=0;i<n;i++){//从0开始
        if(s1[i]=='1' && s2[i]=='1'){//如果当前灯的编号为1,1
            of[cnt_of++]={i,c[i]};
            on[cnt_on++]={i,c[i]};
            vis[i]=1;//标记当前灯为1,1状态
        }
        else if(s1[i]=='1' && s2[i]=='0')of[cnt_of++]={i,c[i]};//如果当前灯的编号为1,0
        else if(s1[i]=='0' && s2[i]=='1')on[cnt_on++]={i,c[i]};//如果当前灯的编号为0,1
    }
    sort(of,of+cnt_of,cmp1);//从大到小排序
    sort(on,on+cnt_on,cmp2);//从小到大排序
    OFF();//从1变0
    ON();//从0变1
    printf("%lld\n",ans);//输出答案
    return 0;
}

直接枚举哪些(1, 1)需要转移, n很小

n \leq 5 \times 10^3
而你的想法是: O(2^n)

转移的一定是大的那部分

还有把你的常规改成问题讨论
image

OK了

我一步一步更你说

不是枚举子集, 是枚举前缀, 因为根据贪心, 要先把代价大的1转成0, 所以枚举一个k, 让1~k里的(1,1)都变成1 ~ 0 ~ 1, 再更新答案, 所以复杂度是 O(n ^ 2)

是在排序之后枚举, 所以没必要把每一种子集都枚举过去

具体的贪心方法是 1,1的和1,0的一起弄,大的先

完整代码

#include<bits/stdc++.h>
using namespace std;
long long n,kms,kj=0;
struct p{
	long long c;
	char a,b;
}c[5005];

bool cmp(p x,p y){
	if(x.c!=y.c)return x.c>y.c;
	return x.a>y.a;
}
set<long long,greater<long long> >s;
set<long long>k;
map<long long,int>S,K;
//k从小到大代价,s从大到小代价,K,S记录个数(set会去重)
signed main(){
	freopen("key.in","r",stdin);
  	freopen("key.out","w",stdout); 
	cin>>n;
	for(long long i=1;i<=n;i++)cin>>c[i].c;
	for(long long i=1;i<=n;i++)cin>>c[i].a;
	for(long long i=1;i<=n;i++)cin>>c[i].b;
	sort(c+1,c+n+1,cmp);//排序
	for(long long i=1;i<=n;i++){
		if(c[i].a!=c[i].b){
			if(c[i].a=='0')K[c[i].c]++,k.insert(c[i].c);
			else s.insert(c[i].c),S[c[i].c]++,kj+=c[i].c;//kj(1->0总和)
		}
	}
	for(long long i=1;i<=n;i++){
		if(c[i].a==c[i].b&&c[i].a=='1'){
			kms+=c[i].c;//前缀和(总和1->0->1代价)
		}
	}
	long long sum=1e18,ans=0;
	long long kk=(kj),ll=(kms);//临时变量
	//1->1不动代价
	for(auto j:s){
//		cout<<j<<" ";
		for(int i=1;i<=S[j];i++){//有S[j]个
			kk-=j;//总和减少
			ans+=kk;
			ans+=ll;//加上本来是一不动的	
		}
	}
//	cout<<kk<<" ";
	for(auto j:k){
		for(int i=1;i<=K[j];i++){//有K[j]个
			kk+=j;
			ans+=ll;
			ans+=kk;//加上本来是一不动的		
		}
	}
	sum=min(sum,ans);//取最小
	for(long long i=1;i<=n;i++){
		if(c[i].a==c[i].b&&c[i].b=='1'){
			ans=0;//清零
			long long kk=(kj+=c[i].c),ll=(kms-=c[i].c);//kk:1->0增加总和,ll:加上本来是一不动的
			s.insert(c[i].c);//插入
			S[c[i].c]++;
			for(auto j:s){
				for(int jk=1;jk<=S[j];jk++){
					kk-=j;
					ans+=kk;
					ans+=ll;				
				}
			}
//			kk=0;
//			cout<<kk<<" ";
			k.insert(c[i].c);
			K[c[i].c]++;
			for(auto j:k){
				for(int jk=1;jk<=K[j];jk++){
					kk+=j;
					ans+=ll;
					ans+=kk;				
				}
			}
			sum=min(sum,ans);
		}
	}
	cout<<sum;
}

看懂了吗?

看我题解,写的非常简洁
题解