A.
一句话题意:在纸上写一个数字 n
,双方每次选择 n
的一个因数,然后划掉 n
并在纸上写下 n减去这个数字的差
使之成为新的 n
,最后写数字 0
的人输。Alice
先手,请请你判断谁会获胜。
Solution:
典型的 SG 函数。打表可以发现若 n
是偶数,则先手必胜;若 n
是奇数,则先手必败。
简单证明:显然 n=1
时必败,若 n
为奇数,则一次操作后必为偶数,而 1
是任何数的因数,所以又会变成奇数,故先手必败;反之,则先手必胜。
B.
简单题意:求一个字符串 s
恰好有 k
种还原方式,即满足 A
,B
,C
三个数字视为字符串拼接起来后的结果为 s
且 A+B=C
。如 s="123"
,那么可以还原出 1+2=3
。不能有前导零,可以为 0
。n<=100
,k<=2
。
Solution:
直接按自然数的顺序枚举即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mx=2e5+5;
int n,k,fac[20],f[20],a[20],cnt;
//贪心好题。
int main() {
// freopen("data.in","r",stdin);
// freopen("own.out","w",stdout);
fac[0]=1; for(int i=1;i<=18;i++) fac[i]=fac[i-1]*10;
scanf("%d%d",&k,&n);
for(ll i=1;;i++) {
ll tmp=i;cnt=0;
while(tmp) a[++cnt]=tmp%10,tmp/=10;
reverse(a+1,a+1+cnt);
for(int j=1;j<=cnt;j++) f[j]=f[j-1]*10+a[j];
int ok=0;
for(int j=1;j<cnt;j++) {
for(int k=j+1;k<cnt;k++) {
if((k-j>1&&!a[j+1])||(cnt-k>1&&!a[k+1])) continue;
ll a=f[j],b=f[k]-f[j]*fac[k-j],c=f[cnt]-f[k]*fac[cnt-k];
if(a+b==c) ok++;
}
}
if(ok==k) {
cout<<i<<endl;
if(!--n) return 0;
}
}
}
C.
一句话题意:有一个货币系统,按贪心策略给取款人发放钞票,即每次都为能取得的面额的最大值。求对于取款金额不超过 b
的方案中,最多能取多少张钞票,输出最大值和相应的取款金额。
Solution:
贪心+递推
好题。
首先由于取款的面额一定,所以选择面额小的钞票越多越好。
引理:对于任意取款金额 c
,若面额最小的钞票取得最多,则取款的张数一定最多。
证明:假设多选一张面额为 1
的钞票,则对于 a_2
剩的钱一定更多,所以 a_2
的张数不会减少,以此类推,直到 a_n
的张数减少,又因为 a_1<a_n
,所以一定更优。证毕。
于是考虑递推,考虑在满足 c<a_i
的前提下以上一次的结果为基础,求到 a_{i-1}
能选到的最大张数。
最后处理答案。二分找到 ps_i<=b<ps_{i+1}
,此时 b<a_{i+1}
,所以不会选择 a_{i+1}
,求出 a_{i}
能选到的最大张数即可。
但是可能存在 b
没有取满的情况,但为了满足引理,此时不能选择更多的 a_{i}
,所以对答案没有影响。
时间复杂度 O(nlogn)
。妙极。
#include<bits/stdc++.h>
#define ll long long
#define mp make_pair
using namespace std;
const int mx=2e5+5;
int n,q;
ll cost,a[mx];
ll dp[mx],ps[mx];
//贪心:尽量选择面额较小的货币
//yy 一下贪心策略的正确性
int main() {
// freopen("data.in","r",stdin);
// freopen("own.out","w",stdout);
scanf("%d",&n);
a[0]=1;
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<=n;i++) {
dp[i]=dp[i-1]+(a[i]-1-ps[i-1])/a[i-1];
ps[i]=ps[i-1]+(a[i]-1-ps[i-1])/a[i-1]*a[i-1];
}
scanf("%d",&q);
// printf("nmsl");
while(q--) {
scanf("%lld",&cost);
int it=upper_bound(ps+1,ps+1+n,cost)-ps-1;
// cout<<it<<" "<<ps[it]<<endl;
printf("%lld %lld\n",ps[it]+(cost-ps[it])/a[it]*a[it],dp[it]+(cost-ps[it])/a[it]);
}
}
D.
题意:给定一个长度为 n
的序列 a
,可以进行 k
次区间翻转操作,求最终序列最多有多少对相邻两项满足 a_i!=a_{i+1}
。
Solution:
这道题不是很严谨,有点偏思维。我的做法很玄学,但是过掉了。
首先确定答案的上界,假设出现次数最多的数字出现了 x
次,若 x>ceil(n/2)
,则最多有 2(n-x)
对数,否则最多 n-1
对数。
然后这道题可以转化为对于一些数,每次选两个减去 1
,最多操作多少次。这样每次操作贡献为 2
。可以每次选最大数和次大数进行操作。最后如果只剩下一个数,就和其他已经匹配好的数操作,每次贡献为 1
。
下面我们来证明一定存在已经匹配的数与之匹配。
首先一定存在已经匹配的数,否则说明只有一种相同的数字,答案上限为 0
。
其次,假如每一对都存在 a
,而此时序列中只存在连续的 a
,可以发现 a
会交错出现,那么假若首位不是 a
,还可以各翻转一次;否则 cnt(a)>ceil(n/2)
,已经达到了答案上界,所以剩下的操作没有贡献。
综上,时间复杂度 O(nlogn)
。
#include<bits/stdc++.h>
#define ll long long
#define mp make_pair
using namespace std;
const int mx=5e5+5;
int n,m,a[mx],d[mx],maxappear,maxpair,cnt,res,res2;
multiset<int,greater<int> > s;
//注意一下终止状态
int main() {
// freopen("data.in","r",stdin);
// freopen("own.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
d[a[i]]++;
}
for(int i=1;i<=n;i++) maxappear=max(maxappear,d[i]);
if(maxappear>(n+1)/2) {
maxpair=2*(n-maxappear);
}
else {
maxpair=n-1;
}
memset(d,0,sizeof(d));
for(int i=2;i<=n;i++) {
if(a[i]==a[i-1]) d[a[i]]++;
else res++;
}
for(int i=1;i<=n;i++) {
if(d[i]) s.insert(d[i]);
}
while(m&&s.size()>1) {
int x=*s.begin();
s.erase(s.begin());
int y=*s.begin();
s.erase(s.begin());
res+=2,m--,x--,y--;
if(x) s.insert(x);
if(y) s.insert(y);
}
int cnt=0;
for(auto x:s) {
cnt+=x;
}
res+=min(m,cnt);
printf("%d",min(res,maxpair));
}