好题整理
2.5
2018A-Card Partition *1600 思维题
- 这道题就是简单的枚举,因为 $n$ 不大。因此就直接枚举每组分成多少张,这样就能算出组数了,这里的总数是把加的牌也算进去的,这样就是看最坏的情况下能否满足题目条件。
void solve(){
cin>>n>>k;
int mx=0,s=0;
for(int i=1;i<=n;i++)cin>>w[i],mx=max(mx,w[i]),s+=w[i];
int ls=s+k;
int ans=0;
for(int i=1;i<=n;i++){
int t=ls/i;//假定分成t份
if(t>=mx&&t*i>=s&&t*i<=ls)ans=i;
}
cout<<ans<<endl;
}
2004D-Colored Portals *1600
真的受不了这种题,看错一个地方调了我一个半小时。
-
思路很简单,因为只有 $4$ 种颜色取 $2$ 种,那么总共也就有 $6$ 种情况。可以推出答案就两种情况——在同一层、不在同一层(可以证明只需要一个辅助城市即可)
-
这里取巧:每种颜色组合在每一层出现的最远位置和最近位置记录下来,这里就直接用
vector<array<int,6>>(n+1,{-1,-1,-1,-1,-1,-1})来记录,就是这里,我没想到。
int n,q;
map<string,int>id{{"BG",1},{"BR",2},{"BY",3},{"GR",4},{"GY",5},{"RY",6},
{"GB",1},{"RB",2},{"YB",3},{"RG",4},{"YG",5},{"YR",6},
};
void solve(){
cin>>n>>q;
vector<int>a(n+1,0);
vector<string>s(n+1);
for(int i=1;i<=n;i++){
cin>>s[i];
a[i]=id[s[i]];
}
vector<array<int,7>>b(n+1,{-1,-1,-1,-1,-1,-1,-1});
for(int i=1;i<=n;i++){
b[i]=b[i-1];
b[i][a[i]]=i;
}
vector<array<int,7>>c(n+2,{-1,-1,-1,-1,-1,-1,-1});
for(int i=0;i<=6;i++)c[n+1][i]=-1;
for(int i=n;i>=1;i--){
c[i]=c[i+1];
c[i][a[i]]=i;
// cout<<a[i]<<' '<<i<<endl;
}
// cout<<c[2][2]<<endl;
while(q--){
int x,y;
cin>>x>>y;
if(x>y)swap(x,y);
if(x==y){
cout<<0<<endl;
continue;
}
int ans=1e18;
if(s[x][0]==s[y][0]||s[x][0]==s[y][1]||s[x][1]==s[y][0]||s[x][1]==s[y][1]){
cout<<abs(x-y)<<endl;
continue;
}
int t1=a[x],t2=a[y];
bool f=false;
// cout<<t1<<' '<<t2<<endl;
for(int i=1;i<=6;i++){
if(i!=t1&&i!=t2){
// cout<<b[x][i]<<' '<<b[y][i]<<' '<<c[x][i]<<' '<<c[y][i]<<endl;
if(b[x][i]!=-1||b[y][i]!=-1||c[x][i]!=-1||c[y][i]!=-1){
f=1;
break;
}
}
}
if(!f){
cout<<"-1"<<endl;
continue;
}
for(int i=1;i<=6;i++){
if(i!=t1&&i!=t2){
if(b[x][i]!=-1){
ans=min(ans,abs(b[x][i]-x)+abs(b[x][i]-y));
}
if(b[y][i]!=-1){
ans=min(ans,abs(b[y][i]-x)+abs(b[y][i]-y));
}
if(c[x][i]!=-1){
ans=min(ans,abs(c[x][i]-x)+abs(c[x][i]-y));
}
if(c[y][i]!=-1){
ans=min(ans,abs(c[y][i]-x)+abs(c[y][i]-y));
}
}
}
cout<<ans<<endl;
}
}
1996E-Decode *1600 前缀和+思维
- $n$ 很大,要求两个变量的方案和,可以想到贡献法。
- 很容易想到对于某一个满足的如 $11111001111$ 如果满足题意,答案就是 $l\times r$。
- 有两个变量不好算,因此枚举一个变量:对于每一个 $l,r$,此时固定 $r$,都有:
$$ans=l_1r+l_2r+l_3r+...+l_kr$$
$$ans=\sum_{i=1}^{k}{l_k}*r$$
因此我们只需要算 $\sum_{i=1}^{k}{l_k}$ 即可,这个我们可以用 map 来解决。
void solve(){
cin>>s;
n=s.size();
s=' '+s;
int ans=0;
map<int,int>S;
S[0]=1;
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+(s[i]=='1'?1:-1);
}
for(int i=1;i<=n;i++){
ans=(ans+(n-i+1)*(S[sum[i]])%mod)%mod;
S[sum[i]]+=i+1;
}
cout<<ans<<endl;
}
2.6水题3+1600题6
1994C-Hungry Games *1600 二分+思维
- 就是一个二分去划分边界,对于一个左端点为 $i$ 的区间,可以分为两部分,一部分为 $a_i$~$a_q$,这部分他们的总和就是 $≤x$,显然不涉及到“归零”这个操作,答案直接加上 $q−i−1$。
accumulate()第三个参数是基准值,也就是 $f_1+f_2+...+f_n+t$,$t$ 就是基准值。
void solve() {
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>w[i],s[i]=s[i-1]+w[i];
vector<int>f(n+2);
for(int i=n;i>=0;i--){
int q=upper_bound(s+1,s+1+n,s[i]+k)-s;//ai~aq
f[i]=f[q]+q-i-1;
}
int ans=0;
cout<<accumulate(f.begin(),f.end(),0ll)<<endl;
}
错误次数:2
错误原因:已经想到二分/双指针去做,但没有深入思考边界的处理。
1978D-Elections *1600 思维+前缀和
思路很清晰,答案就三种情况:0,i,i-1。
void solve() {
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>w[i];
w[1]+=k;
for(int i=1;i<=n;i++)s[i]=s[i-1]+w[i];
for(int i=1;i<=n;i++){
pre[i]=max(pre[i-1],w[i]);
}
for(int i=n;i>=1;i--){
suf[i]=max(suf[i+1],w[i]);
}
int t=*max_element(w+1,w+1+n);
int cnt=0,id=0;
for(int i=1;i<=n;i++){
if(t==w[i]){
cnt++;
}
}
for(int i=1;i<=n;i++){
if(t==w[i]){
id=i;
break;
}
}
if(w[1]>=t){
cout<<"0 ";
}else{
cout<<"1 ";
}
for(int i=2;i<=n;i++){
if(w[i]>=t){
if(w[i]==t&&cnt!=1&&i!=id){
cout<<i-1<<' ';
}else{
cout<<0<<' ';
}
}else{
int ans=i-1;
if(s[i]<t){
ans++;
}
cout<<ans<<' ';
}
}
cout<<endl;
}
错误次数:2
错误原因:要用前缀和去比较
1956C-Nene's Magical Matrix *1600 构造
- 构造方案是行和列的赋值,其实通过多次画图可以知道:
1 2 3 4 5
2 2 3 4 5
3 3 3 4 5
4 4 4 4 5
5 5 5 5 5
- 这种方案是最优的,而且要输出方案也很好写,我们只需要从外到内而且排列都不需要变换。
void solve() {
cin>>n;
int ans=0;
FOR(i,1,n){
ans+=(i*i+(i+1+n)*(n-i)/2);
}
cout<<ans<<' '<<n*2<<endl;
FORD(i,1,n){
cout<<"1 "<<i<<' ';
FOR(j,1,n)cout<<j<<" \n"[j==n];
cout<<"2 "<<i<<' ';
FOR(j,1,n)cout<<j<<" \n"[j==n];
}
}
错误次数:1
错误原因:布置行了后面开始考虑列的时候忽略了行的修改。
1957C-How Does the Rook Move? *1600 排列组合
- 这种就是画个图枚举,可以发现:
-
- 如果你画在对角线上机器人就没有地方画了,此时就是随便放,方案也就是 $C_{n}^{i}$,其中 $n$ 表示之前下完棋剩下棋盘的行数,$i$ 表示下了 $i$ 次放在对角线的棋,这里的 $n-i$ 是偶数,如果不是的情况下,那么会对下面不在对角线的情况下会下不完剩余的列。
-
- 除对角线画,一共有 $C_{n-i}^{j}\times j!$,$j=\frac{n-i}{2}$。
- 那么答案就是二者乘积之和。
int qmi(int a,int b){
int res=1;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b/=2;
}
return res;
}
void init(int x){
fac[0]=infac[0]=1;
FOR(i,1,x){
fac[i]=fac[i-1]*i%mod;
}
infac[x]=qmi(fac[x],mod-2);
FORD(i,1,x-1){
infac[i]=infac[i+1]*(i+1)%mod;
}
}
int C(int x,int y){
return fac[x]*infac[y]%mod*infac[x-y]%mod;
}
void solve() {
cin>>n>>k;
map<int,int>S;
FOR(i,1,k){
cin>>x[i]>>y[i];
S[x[i]]=S[y[i]]=1;
}
n=n-S.size();
int ans=0;
FOR2(i,n&1,n){
int j=(n-i)/2;
ans=(ans+C(n,i)*C(n-i,j)%mod*fac[j]%mod)%mod;
}
cout<<ans<<endl;
}
1971F-Circle Perimeter *1600 数学
服了,我怎么会一直往那个 皮克定理 那边去想
- 公式推导:
$$r\le \sqrt{x2+y2} < r+1$$
$$r^2\le x2+y2<(r+1)^2$$
$$r2-y2\le x^2 <(r+1)2-y2$$
$$\sqrt{r2-y2}\le x<\sqrt{(r+1)2-y2}$$
-
那么 $x_{min}=\lfloor \sqrt{r2-y2} \rfloor$ ,$x_{max}=\lceil\sqrt{(r+1)2-y2} \rceil$
-
此时直接枚举 $y$ 就可以算出 $x$ 了,最后答案 $\times 4$ 就可以了。
void solve() {
cin>>n;
int ans=0;
for(int i=1;i<=n;i++){
int mx=floor(sqrt((n+1)*(n+1)-i*i-0.5));
int mn=ceil(sqrt(n*n-i*i));
ans+=mx-mn+1;
}
cout<<ans*4<<endl;
}
错误次数: 1
错误原因:皮克定理是用来算多边形的面积跟整点数的关系,圆的面积不是定数,如果要用的话很难取 $eps$,因此最好不用。
1976C-Job Interview *1600 思维
- 总共的职位数只有 $n+m$ 个,但总共有 $n+m+1$ 个人,为了更好的讨论,这里直接不选最后一个人,此时就很好做了,用的是贪心。
- 这里我是用 $vis_i$ 表示第 $i$ 个人被选上测试职业,$ca$ 表示选了 $ca$ 个程序员,$f_i$ 表示该人不被选上个人可以多获得的价值。
- 此时的 $f_i$ 此时就是一个差值(变化量),默认不选最后一个人的答案是 $ans$。
- 然后,发现如果改为不选第 $i$ 个人,实际上就是让下一个原本要选某一个职业但是没法选的人选某一个职业 然后还要有下一个人顶替他选的岗位,以此类推。
void solve() {
cin>>n>>m;
for(int i=1;i<=n+m+1;i++)cin>>a[i];
for(int i=1;i<=n+m+1;i++)cin>>b[i];
vector<int>f(n+m+2,0),vis(n+m+2,0);
int ca=0,cb=0,ans=0;
for(int i=1;i<=n+m;i++){//默认不选最后一个
if(ca==n)ans+=b[i],vis[i]=0;
else if(cb==m)ans+=a[i],vis[i]=1;
else if(a[i]>b[i])vis[i]=1,ans+=a[i],ca++;
else ans+=b[i],vis[i]=0,cb++;
}
ca=n+m+1,cb=n+m+1;
for(int i=n+m;i;i--){
if(ca==n+m+1&&vis[i])f[i]=f[ca]+a[ca];
else if(cb==n+m+1&&!vis[i])f[i]=f[cb]+b[cb];
else if(vis[i])f[i]=f[ca]+a[ca]-b[ca];
else f[i]=f[cb]+b[cb]-a[cb];
if(vis[i]&&b[i]>a[i])cb=i;
else if(!vis[i]&&b[i]<a[i])ca=i;
}
for(int i=1;i<=n+m;i++){
cout<<(ans-(vis[i]?a[i]:b[i])+f[i])<<' ';
}
cout<<ans<<endl;
}
没想出来这么做,憨了
HDU-6702-& 位运算
- 容易发现要让答案最小,对每一位进行分析很容易发现,当 $a,b$ 在同一位均为 $1$ 的情况下,此时 $c$ 填 $1$.
void solve() {
cin>>a>>b;
int ans=0;
for(int i=34;i>=0;i--){
if((a>>i&1)&&(b>>i&1)){
ans|=(1ull<<(i));
}
}
if(ans==0){
if(((a^0)&(b^0))==0){
cout<<1<<endl;
}else{
cout<<0<<endl;
}
}else{
cout<<ans<<endl;
}
}
HDU-6707-Shuffle Card STL:栈
- 没什么好说的。
void solve() {
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>w[i];
stack<int>stk;
while(m--){
int x;
cin>>x;
stk.push(x);
}
while(stk.size()){
int x=stk.top();
stk.pop();
if(!vis[x]){
cout<<x<<' ';
}
vis[x]=1;
}
for(int i=1;i<=n;i++){
if(!vis[w[i]]){
cout<<w[i]<<' ';
}
}
}
HDU-6708-Windows Of CCPC找规律
- 根据这个:
和:
-
又因为数组的大小是 $2$ 的幂次方且都是从 $2^{n-1}$ 得到,因此我们可以对上述字母分块,我们可以很容易发现:第一块和第四块是相同的,第二块和第三块是相同的。
-
块数划分编号:
1 2 3 4
- 然后直接打表就可以了
void init(){
s[1][1]=s[1][2]=s[2][2]='C';
s[2][1]='P';
for(int i=2;i<=10;i++){
int l=pow(2,i-1);
int r=pow(2,i);
for(int j=1;j<=l;j++){
for(int k=l+1;k<=r;k++){
s[j+l][k]=s[j][k]=s[j][k-l];
}
}
for(int j=1;j<=l;j++){
for(int k=1;k<=l;k++){
s[j+l][k]=(s[j][k]=='C'?'P':'C');
}
}
}
}
void solve() {
cin>>n;
n=pow(2,n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cout<<s[i][j];
}
cout<<endl;
}
}
2.7 1600-1800乱刷
今天感觉什么事情都没干,昨晚睡太晚导致今天早上起不来,然后打算下午睡个觉,没想到又浪费了很多时间,服了,今天约等于也就只有5个小时刷题时间,太少了,而且做的又少,难受,今晚又做题做累了,明天又很冷,不知道会不会起得来。
这几天计划是刷1600-2400的题,康复训练,已经很久没有做 codeforces 的题了。
1946C-Tree Cutting *1600 二分+dfs
- 这个一看肯定跟子树有关系,因此就得维护
siz数组。 - 如果选某一条边,此时就让它的
siz清零,这样不会对后面产生影响。 - 题目的答案具有单调性(如果给的答案太大了,可以通过缩小区间来减小答案的范围),可以很容易写出二分。
void solve() {
cin>>n>>k;
VII g(n+1);
FOR(i,1,n-1){
int a,b;
cin>>a>>b;
g[a].emplace_back(b);
g[b].emplace_back(a);
}
vector<int>siz(n+1,0);
int cnt=0;
function<void(int,int,int)> dfs=[&](int u,int fa,int lim){
siz[u]=1;
for(auto& j:g[u]){
if(j==fa)continue;
dfs(j,u,lim);
siz[u]+=siz[j];
}
if(siz[u]>=lim){
cnt++;
siz[u]=0;
return;
}
if(u==1&&siz[u]<lim&&cnt==k)cnt=0;
};
function<bool(int)>check=[&](int x){
siz.clear();
cnt=0;
dfs(1,0,x);
return cnt<=k;
};
int l=0,r=n+1;
while(l+1!=r){
int mid=l+r>>1;
if(check(mid))r=mid;
else l=mid;
}
cout<<l<<endl;
}
总结:思路很简单的,认真考虑是可以很快的想出来。
2020C-Bitwise Balancing *1600 位运算
- 这道题就直接枚举每一位做,然后发现如果在第 $i$ 位下,$b=1,c=0,d=1$,这种情况是允许的,还有就是 $b=0,c=1,d=1$ 也是允许的,其他情况均不行。
void solve() {
cin>>b>>c>>d;
int a=0;
FOR(i,0,61){
int x,y,z;
x=((b>>i)&1);
y=((c>>i)&1);
z=((d>>i)&1);
if(x!=z){
if(1-y!=z){
cout<<-1<<endl;
return;
}
a|=(1ll<<(i));
}
}
cout<<a<<endl;
}
总结:位运算的题很多情况下都得按位看,然后有的时候可能会用枚举答案的思想来做题。
2035C-Alya and Permutation *1600 构造+位运算
- 这种题你乍一看很没有思路,此时可以发现不同点:奇数和偶数它们的答案是不同的。这点可以通过样例来看。
- 因为奇数最后一次操作是与,因为与会使答案$\le n$,所以盲猜答案是 $n$。
- 因为偶数最后一次操作是或,或能让答案 $>=n$,所以很容易知道答案肯定是 $2^{\lfloor{\log_{2}n}\rfloor}-1$。
- 其实最关键的是答案的构造,这里分类讨论。
-
- 奇数:因为最后一次操作是与,然后最后一位数肯定是与 $n$,那么 $n$ 的前面操作也要使得结果为 $n$,要不然结果就不会等于 $n$ 了。发现 $1,n-1,n$ 这种组合可以,因为是先与 $1$,此时你不管前面如何,最低位必为 $1$(因为 $n$ 是奇数)。
-
- 偶数:因为最后一次操作是或,那么你要让答案等于 $2^{\lfloor{\log_{2}n}\rfloor+1}-1$,就得让除最高位外其余的均为 $1$,然后在此数前面的只需要提供最高位即可。那么 $n,n-1,2^{\lfloor{\log_{2}n}\rfloor}-1$ 满足要求。
-
- 偶数还有一种特殊情况,就是很有可能 $n=2^{\lfloor{\log_{2}n}\rfloor}$,那么前面就不能有 $n-1$,此时 $1,n-3,n-2,n-1,n$ 满足条件。因为 $n-1$ 的二进制就是全 $1$ 的数字,但你肯定不能让 $n-1$ 消失,又因为 $n-1$ 前的操作符是与,为了让 $n-1$ 能够完整的传承下去,那么必须得让前面的数字的结果为 $n-1$,那么 $1,n-3,n-2$ 符合条件。
就是这道题,浪费了我1个多小时的时间,真憨了。
- 最后的代码也是特别的短。
void solve() {
cin>>n;
if(n&1){
cout<<n<<endl;
FOR(i,1,n-4)cout<<i+1<<' ';
cout<<1<<' '<<n-2<<' '<<n-1<<' '<<n<<endl;
}else{
int t=log2(n);
cout<<(int(pow(2,t+1))-1)<<endl;
if((int)pow(2,t)==n){
FOR(i,1,n-5)cout<<i+1<<' ';
cout<<1<<' '<<n-3<<' '<<n-2<<' '<<n-1<<' '<<n<<endl;
}else{
FOR(i,1,(int)(pow(2,t)-2))cout<<i<<' ';
FOR(i,(int)pow(2,t),n-2)cout<<i<<' ';
cout<<n<<' '<<n-1<<' '<<(int)pow(2,t)-1<<endl;
}
}
}
错误原因:没有认真考虑偶数的关系,偶数还有一种情况没有考虑到。
1932E-Final Countdown*1600 贡献法+高精度
- 思路特别简单,就是去算:比如123,答案就是123+12+1。
- 但这边得优化一下,就是按每一位来做,此时前缀和来做就可以了,前缀和是维护位数数字的和的。
void solve() {
cin>>n>>s;
VI sum(n),C;
sum[0]=s[0]-'0';
FOR(i,1,n-1){
sum[i]=sum[i-1]+s[i]-'0';
}
int t=0;
FORD(i,0,n-1){
t+=sum[i];
C.pb(t%10);
t/=10;
}
if(t)C.pb(t);
while(C.sz>1&&C.back()==0)C.pop_back();
reverse(ALL(C));
for(auto x:C)cout<<x;
cout<<endl;
}
错误次数:1
错误原因:没有对操作优化,单纯相加很耗时的。
1928C-Physical Education Lesson *1600 数学
-
这道题可以很容易知道:题目的函数的周期 $T=2(k-1)$。
-
然后要求的是 $k$,然后可以知道求的东西是: $$n\equiv x (\mod 2(k-1))$$
-
以及: $$n\equiv 2k-x (\mod 2(k-1)) \iff n\equiv 2-x(\mod 2(k-1))$$
-
所以题目就转化成了求满足以上两个式子不同的 $k$ 的个数。
-
特别知道:$a\equiv b(\mod x)$,求 $x$ 的个数,那么也就是求 $a-b|x$的个数,此时就转化成了求 $a-b$ 因子的个数。
-
注意:当 $x=k$ 的时候,会多算一次;因子必须是偶数,因为是 $2(k-1)$,这个数是偶数。
bool check(int i,int b,int x){
int k=i/2+1;
if(!(i&1)&&i>=2&&(k>=x)&&!(b!=x&&(k==x)))return 1;
return 0;
}
int calc(int a,int b,int x){
int p=a-b,res=0;
for(int i=1;i<=p/i;i++){
if(p%i==0){
if(check(i,b,x))res++;
if(i!=p/i){
if(check(p/i,b,x))res++;
}
}
}
return res;
}
void solve() {
cin>>n>>x;
int res=calc(n,x,x);
if(x!=1)res+=calc(n,2-x,x);
cout<<res<<endl;
}
总结:卡在黑体部分内容。
510C-Fox And Names *1600 拓扑序
- 又是一道把字母转化到图的题目。
void solve() {
cin>>n;
FOR(i,1,n)cin>>s[i];
string s2=s[1],s1;
VII g(300);
VI din(300,0),ans;
FOR(i,2,n){
s1=s[i];
int a=0;
int m=min<int>(s1.sz,s2.sz);
FOR(j,1,m){
if(s1[a]!=s2[a]){
break;
}
a++;
}
if(a>=m&&s2.sz>s1.sz){
cout<<"Impossible"<<endl;
return;
}
// cout<<s2[a]<<' '<<s1[a]<<endl;
if(s2[a]>='a'){
g[s2[a]].emplace_back(s1[a]);
din[s1[a]]++;
}
s2=s1;
}
queue<int>q;
FOR(i,'a','z'){
if(!din[i])q.push(i);
}
while(q.sz){
int t=q.front();
q.pop();
ans.pb(t);
for(auto x:g[t]){
--din[x];
if((din[x])==0){
q.push(x);
}
}
}
if(ans.sz<26){
cout<<"Impossible"<<endl;
}else{
for(auto x:ans)cout<<char(x);
}
}
总结:对于很多字符的题目要找什么关系的时候,往往会转化到图
(还记得ICPC杭州区域赛A题用的也是字符串的关系建图)
Maximum AND *1800 位运算
不得不说位运算的挺有意思的,出题人总有很特殊的角度进行考察。
- $b$ 数组是可以任意排序的。
- 因为是先异或再与的,可以发现:要让最后的结果最大,那肯定对于每一位来说都要有 $1$ 是最好的。
- 可以发现:$0 \^\ 1 =1$,那么可以知道 $a$ 数组的 $0$ 的个数是可以跟 $b$ 数组的 $1$ 数组是相等的(前提是在第 $i$ 位的情况下),反过来也相等。
- 那么也就是说匹配是相同的。
- 此时就可以先枚每一位,如果答案的这一位要是 $1$ 的情况下,那么必须让 $a$ 数组和 $b$ 数组的匹配是相同的。
bool check(int t){
VI x,y;
FOR(i,1,n){
x.pb(a[i]&t);
y.pb((b[i]&t)^t);
}
sort(ALL(x));
sort(ALL(y));
return x==y;
}
void solve() {
cin>>n;
FOR(i,1,n)cin>>a[i];
FOR(i,1,n)cin>>b[i];
int ans=0;
FORD(i,0,30){
if(check(ans|(1ll<<i))){
ans|=(1<<i);
}
}
cout<<ans<<endl;
}
错误原因:没有想到每一位的匹配是相同的。
2.8 专题速练+水题乱刷
今天特别冷,我靠,手放在键盘上一种冻僵的感觉很难受,所以今天的做题体验是特别的差,但今天算下来我也收获了挺多。
后面决定启动仪式的时候得做几道水题,要不然没有状态。
1920C-Partitioning the Array *1600 数学
- 要分成 $k$ 份,那么 $k$ 必须是 $n$ 的因数。
- 要让每个组的数相同,那么也就是让 $a_i \equiv a_{i+k}(\mod m)$,那么算这个之前做过类似的,就是变成: $a_i -a_{i+k}\equiv 0(\mod m)$,也就是 $|a_i-a_{i+k}|$ 能被 $m$ 整除,那么 $m$ 也就是它的因数。
- 要求 $m$ 的个数也就是求 $|a_i-a_{i+k}|$ 的最小公倍数。
- 如果最小公倍数等于 $1$ 则不满足条件。
bool check(int x){
int t=0;
FOR(i,x+1,n){
t=__gcd(t,abs(w[i]-w[i-x]));
if(t==1)return false;
}
return 1;
}
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
int res=0;
for(int i=1;i<=n/i;i++){
if(n%i==0){
if(check(i))res++;
if(i!=n/i&&check(n/i))res++;
}
}
cout<<res<<endl;
}
最关键的是得考虑多个数都能被 $x$ 整除,那么 $x$ 是它们的最小公倍数。
1899F-Alex's whims *1600 思维+构造
- 这就是骗分题,直接让树是一条链,此时的边长是 $n-1$。
- 然后如果 $d<n-1$,此时就断开 $(n,n-1)$,连接 $(n,d)$
void solve() {
cin>>n>>m;
FOR(i,1,n-1)cout<<i<<' '<<i+1<<endl;
int t=n-1;
while(m--){
int d;
cin>>d;
if(d==t){
cout<<"-1 -1 -1"<<endl;
}else{
cout<<n<<' '<<t<<' '<<d<<endl;
t=d;
}
}
}
最关键的就是能想到是一条链,然后在链条上修修补补就可以了。因此对于构造题,可以想象特殊情况是什么,这样就能更好的构造了。(
千万不要被难度吓到,其实有的难度是虚高的)
1801A-The Very Beautiful Blanket *1600 构造
- 嘿嘿,我直接看样例发现这样构造可以满足:
$$ 0\ 1 \ 4\ 5$$
$$2\ 3 \ 6 \ 7$$
$$512 \ 513 \ 516 \ 517$$
$$514 \ 515 \ 518 \ 518$$
- 这个也很容易证明:$a\oplus a+1 \oplus a+2 \oplus a+3 =0$(
这个非常重要,很多时候都会用到)
void solve() {
cin>>n>>m;
cout<<n*m<<endl;
w[1][1]=0,w[1][2]=1,w[2][1]=2,w[2][2]=3;
FOR2(i,3,n){
w[i][1]=w[i-2][1]+512,w[i][2]=w[i-2][2]+512;
w[i+1][1]=w[i-1][1]+512,w[i+1][2]=w[i-1][2]+512;
}
FOR2(j,3,m){
FOR(i,1,n){
w[i][j]=w[i][j-2]+4;
w[i][j+1]=w[i][j-1]+4;
}
}
FOR(i,1,n){
FOR(j,1,m){
cout<<w[i][j]<<' ';
}
cout<<endl;
}
}
构造题哈哈哈。
1707A-Doremy's IQ *1600 贪心+正难则反思想
- 这道题一上来其实可以知道对于每个物品有 选 和 不选 两种情况,对于这种情况,通常的方法有:
dfs,01背包,贪心。 - 发现数据规模大,因此选择贪心。
- 如果选择正向贪心的话,可以发现后效性非常严重,根本不确定某个比赛到底要不要选,因此考虑反向贪心。
- 可以无脑先选最后一个,然后往前推,直到智商增加到了 $k$。
Q:那为什么这样贪心能保证没有后效性呢?
A:1. 反向能使得智商没有得到浪费。
能使得智商不减的操作(见代码的注释
//处)变得更多。如果正着来的话起点的选择是多样的。
void solve() {
cin>>n>>q;
FOR(i,1,n)cin>>w[i];
int k=0;
string ans;
ans.resize(n+1);
FORD(i,1,n){
if(k>=w[i])ans[i]='1';//这个会更多
else if(k<q)ans[i]='1',k++;
else if(k>q)break;
}
FOR(i,1,n)cout<<(ans[i]?1:0);
cout<<endl;
}
1137B-Camp Schedule *1600 KMP
- 如果纯纯贪心得考虑一种情况
10101,这个是被算作两次的101。 - 如果单纯贪心的话这种情况是考虑不到的。
- 因此就得用 kMP 的
next数组了。 - 此时如果匹配到了
101,那么就用next数组,此时next=1,这样就不会出现输出结果为:101101了。
void solve() {
scanf("%s %s",a+1,b+1);
n=strlen(a+1),m=strlen(b+1);
int j=0;
FOR(i,2,m){
while(j&&b[j+1]!=b[i])j=ne[j];
if(b[j+1]==b[i])j++;
ne[i]=j;
}
FOR(i,1,n)cnt[a[i]]++;
int t=1;
FOR(i,1,n){
if(cnt[b[t]])ans[i]=b[t],cnt[ans[i]]--;
else ans[i]=(b[t]^1),cnt[ans[i]]--;
if(t==m)t=ne[t];
t++;
}
FOR(i,1,n)cout<<ans[i];
cout<<endl;
}
错误原因:没有想到 KMP 还可以这样被应用,
next数组用法。
346C-Lucky Common Subsequence *2000 dp+KMP
- 如果没有 $virus$ 数组的话,那么这道题就是很典型的
LCS的题目,dp 方程也就很好写了:f[i][j]表示 $s_1$ 前 $i$ 个字符和 $s_2$ 前 $j$ 个字符能匹配的最长公共子序列,转移也就很好写了。 - 这道题多一个匹配,匹配就得请 KMP 出场了,此时
f[i][j][k]表示:设当第一个串匹配到 $i$ 位,第二个串匹配到 $j$ 位时,能匹配到第三个串的 $k$ 位的最长公共子序列的方案。 - 状态转移方程:
- $f_{i,j,t}=max{f_{i,j,t},f_{i-1,j-1,k}+s_1{i}}f=max{f_{i-1,j,k},f_{i,j-1,k},f_{i,j,k}}$
- 这里特别要注意一下范围,范围是 $[0,virus.size()-1]$,为什么呢?因为可以不匹配也可以最多匹配到最后一个字母的前一个字母。
string max(string a,string b){
if(a.sz>b.sz){
return a;
}else{
return b;
}
}
void solve() {
cin>>s1>>s2>>s3;
la=s1.sz,lb=s2.sz,lc=s3.sz;
s1=' '+s1,s2=' '+s2,s3=' '+s3;
if(la>lb){
swap(la,lb);
swap(s1,s2);
}
int j=0;
FOR(i,2,lc){
while(j&&s3[j+1]!=s3[i])j=ne[j];
if(s3[j+1]==s3[i])j++;
ne[i]=j;
}
FOR(i,1,la){
FOR(j,1,lb){
FOR(k,0,lc-1){
if(s1[i]==s2[j]){
int t=k;
while(t>0&&s1[i]!=s3[t+1]){
t=ne[t];
}
if(s1[i]==s3[t+1])t++;
f[i][j][t]=max(f[i][j][t],f[i-1][j-1][k]+s1[i]);
}
f[i][j][k]=max(f[i][j][k],f[i-1][j][k]);
f[i][j][k]=max(f[i][j][k],f[i][j-1][k]);
}
}
}
string ans;
FOR(i,0,lc-1){
if(f[la][lb][i].sz>=ans.sz){
ans=f[la][lb][i];
}
}
if(ans.sz==0){
cout<<0<<endl;
}else{
cout<<ans;
}
}
类似这种匹配的还是得把 KMP 放在考虑的范围上。
59E-Shortest Path *2000 最短路+hash+路径记录
- 就是一个最短路,因为边权为 $1$,直接用
bfs做。 - 还有就是得记录路径(这部分是难点)
void solve() {
cin>>n>>m>>k;
FOR(i,1,m){
int a,b;
cin>>a>>b;
g[a].emplace_back(b);
g[b].emplace_back(a);
}
FOR(i,1,k){
int a,b,c;
cin>>a>>b>>c;
S.insert({a,b,c});
}
PII ans={-1,-1};
queue<array<int,2>>q;
q.push({1,1});
while(q.sz){
auto [u,v]=q.front();
q.pop();
if(v==n){
ans={u,v};
break;
}
for(auto j:g[v]){
if(S.count({u,v,j})||p[v][j])continue;
p[v][j]=u;
q.push({v,j});
}
}
if(ans.fi==-1){
cout<<"-1"<<endl;
return;
}
VI res;
while(ans.se!=1){
res.push_back(ans.se);
ans={p[ans.fi][ans.se],ans.fi};
}
cout<<res.sz<<endl;
res.push_back(1);
reverse(ALL(res));
for(auto x:res)cout<<x<<' ';
}
最关键是路径记录。
653E- Bear and Forgotten Tree 2 *2400 bfs+思维
- 不满足条件的特判很好写,略。
- 此时把 $1$ 去掉,看其它的点相互连边(排除掉禁止连边的边),如果其他点相互连接的产生的连通块比 $k$ 大,此时就说明 $1$ 就得向这些连通块连超过 $k$ 条边的边。
- 这里用
bfs去算连通块,这里偷个懒,直接用set把所有的点加进去,如果在一个连通块内,就把这些点全部删掉,如果删掉剩下的点跟 $1$ 有边相连,说明这个连通块是靠近 $1$ 相邻点,此时这个点就不能被 $1$ 访问到,此时就不是一棵树了。
void bfs(int u){
queue<int>q;
VI t;
q.push(u);
while(q.size()){
int v=q.front();
q.pop();
for(auto j:S){
if(!g[v].count(j)){
t.pb(j);
q.push(j);
}
}
while(t.sz)S.erase(t.back()),t.pop_back();
if(!g[v].count(1))f=false;
}
}
void solve() {
cin>>n>>m>>k;
int d=0;
FOR(i,1,m){
int a,b;
cin>>a>>b;
g[a][b]=1;
g[b][a]=1;
if(a==1||b==1)d++;
}
if(n-d-1<k){
cout<<"impossible"<<endl;
return;
}
FOR(i,2,n){
S.insert(i);
}
int cnt=0;
FOR(i,2,n){
if(S.count(i)){
S.erase(i);
f=1;
bfs(i);
cnt++;
if(f){
cout<<"impossible"<<endl;
return;
}
}
}
if(cnt<=k)cout<<"possible"<<endl;
else cout<<"impossible"<<endl;
}
典型的连通块问题,可以用
bfs、 并查集 、targin等来做。
1923A-Moving Chips
- 水题:就是简单统计 $1$ 直接 $0$ 的个数。
void solve() {
cin>>n;
FOR(i,1,n){
cin>>w[i];
s[i]=s[i-1]+(w[i]==0);
}
int l=0,r=n;
FOR(i,1,n){
if(w[i]==1){
l=i;
break;
}
}
FORD(i,1,n){
if(w[i]==1){
r=i;
break;
}
}
cout<<s[r]-s[l-1]<<endl;
}
1879A-Rigged!
- 水题,直接让答案等于第一个数的第一个数。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i].fi>>w[i].se;
int ans=1e18;
FOR(i,2,n){
if(w[i].se>=w[1].se&&w[i].fi>=w[1].fi){
cout<<"-1"<<endl;
return;
}
}
cout<<w[1].fi<<endl;
}
Fire Again *1500 多路bfs
- 很好写的,本题最坑的就是得用
freopen("","",stdin/stdout),因为在文件。
void solve() {
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
cin>>n>>m>>k;
queue<array<int,3>>q;
FOR(i,1,k){
int x,y;
cin>>x>>y;
vis[x][y]=1;
q.push({x,y,0});
}
FOR(i,1,n){
FOR(j,1,m){
w[i][j]=1e9;
}
}
while(q.sz){
auto [x,y,step]=q.front();
q.pop();
w[x][y]=min(w[x][y],step);
FOR(i,0,3){
int a=dx[i]+x,b=dy[i]+y;
if(a<1||b<1||a>n||b>m||vis[a][b])continue;
vis[a][b]=1;
q.push({a,b,step+1});
}
}
int t=0;
FOR(i,1,n){
FOR(j,1,m){
t=max(t,w[i][j]);
}
}
FOR(i,1,n){
FOR(j,1,m){
if(t==w[i][j]){
cout<<i<<' '<<j<<endl;
return;
}
}
}
}
有的不是标准输入输出,就得用
freopen。
Gym-100866A-Anti factorial 高精度/小技巧
- 哈哈哈,其实不用高精度也可以做,可以:
void solve() {
cin>>s;
int t=0;
FOR(i,0,s.sz-1){
t=(t*10+s[i]-'0')%mod;
}
fac[0]=1;
FOR(i,1,255){
fac[i]=fac[i-1]*i%mod;
}
FOR(i,1,255){
if(t==fac[i]){
cout<<i<<endl;
return;
}
}
}
- 高精度做法:
void solbve(){
VI p;
p.pb(1);
VI pd;
for(auto x:s){
pd.push_back(x-'0');
}
FOR(i,1,255){
int t=0;
VI c;
for(int j=0;j<p.sz||t;j++){
if(j<p.sz)t+=p[j]*i;
c.pb(t%10);
t/=10;
}
p=c;
reverse(ALL(c));
if(c==pd){
cout<<i<<endl;
return;
}
}
}
1735D-Meta-set *1700 思维
-
可以很容易发现,对于 $$a:\ 0 \ 1 \ 2$$ $$b:\ 0 \ 2 \ 1$$
-
那么 $c$ 串也就可以很容易判断出来是 $0 \ 0 \ 0$。
-
题目说只要 $>1$ 个满足这样的,就可以认为是符合条件的。
-
因为 $n\le 10^3$ ,所以可以直接先枚举其中的两个,由于这些数字最大也就 $2$,因此可以想到三进制,由于要定位第三个串,所以必须要让每个串能被十进制数表示,因此就得用三进制来维护。
-
那么为了使该五元集合法,五元集中必须能提取出至少两个的合法三元集。我们之前已经求出 $sum$ 数组,记录含有 $i$ 串的合法三元集的个数。于是当前第 $i$ 串对结果的贡献就是 $C_{sum_i}^2$。
int calc(int x,int y){
int res=0;
for(int i=1,t=1;i<=m;i++,t*=3){
if(w[x][i]==w[y][i]){
res+=w[x][i]*t;
}else{
res+=(3-w[x][i]-w[y][i])*t;
}
}
return res;
}
void solve() {
cin>>n>>m;
FOR(i,1,n){
int t=1,s=0;
FOR(j,1,m){
cin>>w[i][j];
a[i]+=w[i][j]*t;
t*=3;
}
mp[a[i]]=i;
}
FOR(i,1,n-2){
FOR(j,i+1,n-1){
int t=calc(i,j);
if(mp[t]>j){
sum[i]++;
sum[j]++;
sum[mp[t]]++;
}
}
}
int ans=0;
FOR(i,1,n){
ans+=sum[i]*(sum[i]-1)/2;
}
cout<<ans;
}
数字不大,要定位第三个的话,可以考虑进制。
803D-Magazine A *1900 二分
- 这道题就是直接二分答案就可以了,如果遇到
-或 ``的话,考虑换行,然后贪心就可以了。 - 题目具有单调性,二分生效。
bool check(int x){
int c1=1,c2=0,c3=0;
FOR(i,0,s.sz-1){
if(s[i]==' '||s[i]=='-')c3=i;
c2++;
if(c2>=x&&i!=s.sz-1){
if(!c3)return false;
c1++;
c2=i-c3;
c3=0;
}
}
return c1<=n;
}
void solve() {
cin>>n;
int mx=0;
cin.ignore();
getline(cin,s);
int l=0,r=1e12;
while(l+1!=r){
int mid=l+r>>1;
if(check(mid))r=mid;
else l=mid;
}
cout<<r<<endl;
}
883I-Photo Processing *1900 二分+dp
- 题目说了最大值最小,所以就是二分没错了。
- 然后现在是二分,此时也就是给定了答案,如果贪心去分配的话,感觉是有后效性的,因此就得用
dp去做。 - 因为题目只要求分组,不需要具体方案,可以将输入的数排序,然后定义一个 $f_i$ 存的是前 $i$ 个数能满足条件的最后一个数的位置,那么只需判断
f[n]是否等于 $n$。 - 转移:从前往后枚举,因为要求每组至少 $m$ 个数,对于当前的第 $i$ 个数,要使每个数都明确得分到一个组,看
f[i−m]存的前一位,即没分到组的第一个数,如果a[i]−a[t+1]小于mid则可以分为一组更新f[i]=i。
bool check(int x){
VI f(n+1,0);
int pos=0;
FOR(i,m,n){
int t=f[i-m];
if(w[i]-w[t+1]<=x)pos=i;
f[i]=pos;
}
return f[n]==n;
}
void solve() {
cin>>n>>m;
FOR(i,1,n)cin>>w[i];
sort(w+1,w+1+n);
int l=-1,r=w[n]-w[1]+1;
while(l+1!=r){
int mid=l+r>>1;
if(check(mid))r=mid;
else l=mid;
}
if(check(l))r=l;
cout<<r<<endl;
}
到这里一天又结束了,感慨时间过得真快,我花了 $2$ 小时也终于写到了这里。
2.9-2.14 26
期间发生了许多事情,其中有 $3$ 天时间把这个刷题给搁置了,因为流感是真的严重,在修养身体。
2.14统一更新:
706B-Interesting drink *1100
- 没什么说的,二分的水题。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
sort(w+1,w+1+n);
cin>>q;
while(q--){
int x;
cin>>x;
cout<<upper_bound(w+1,w+1+n,x)-w-1<<endl;
}
}
1198B-Welfare State *1600 思维 值得重构
-
看题目,有单点修改和区间修改以及查询,因此很容易可以想到线段树。
-
但我们可以仔细考虑一下,发现有的操作是无用的,这样的思维就可以让我们离线处理查询。
-
比如补平均以下的 $2$,然后我此时的值是 $5$,那么此时这种操作就失效了。
-
还有一个性质就是:数字单调不降,除修改外。
-
综合以上性质,可以很容易写出:
void solve() {
cin>>n;
FOR(i,1,n){
cin>>w[i].fi;
w[i].se=0;
}
cin>>Q;
q[0]=-1e18;
FOR(i,1,Q){
int opt,p,x;
cin>>opt;
if(opt==1){
cin>>p>>x;
w[p]={x,i};
q[i]=-1e18;
}else{
cin>>x;
q[i]=x;
}
}
int mx=-1e18;
FORD(i,0,Q){
mx=max(mx,q[i]);
pre[i]=mx;
}
FOR(i,1,n){
cout<<max(pre[w[i].se],w[i].fi)<<' ';
}
}
线段树做法更容易。
反思:本来有这种离线处理的思想,但总是往排序那边想了,可能是被分块的题目误导了,分块的题目经常要把查询进行排序。
这道题其实只需要倒叙来做就可以了,倒叙维护最大值。
1239A-Ivan the Fool and the Probability Theory *1700 找规律
-
如果只有一行的情况,如果是 $1\times1$ ,答案为 $2$,如果是 $1\times 2$ ,答案为 $4$。
-
那么可以很容易发现如果是 $1\times n$ 的情况,答案就是斐波那契数列的 $2f_1+2f_{n}-2$。
-
至于证明,
其实不太会。 -
简略证明为什么是斐波那契数列:
令 $f_{i,0}$ 为第 $i$ 行涂黑色的方案数, $f_{i,1}$ 为第 $i$ 行涂白色的方案数。
由于同一个颜色的最多只能有 $1$ 个相邻,那么看前面几行可以得出:
$$f_{i,0}=f_{i-1,0}+f_{i-2,0}$$
$$f_{i,1}=f_{i-1,1}+f_{i-2,1}$$
所以:
$$f_{i}=f_{i-1}+f_{i-2}$$
证明完毕。
void solve() {
f[0]=f[1]=1;
cin>>n>>m;
FOR(i,2,max(n,m)+2){
f[i]=(f[i-1]+f[i-2])%mod;
}
cout<<2*(f[n]+f[m]-1)%mod<<endl;//-1就是同色相邻的情况
}
如果出什么求方案数,它很有可能是排列组合或者通过找规律来得出结论的。
1364C-Ehab and Prefix MEXs *1600 构造题
-
这是一道 $mex$ 的题目,mex的性质有单调不降,然后当 $w_i \le i$,其中 $w_i$ 指的是当前 mex 值。
-
特判很容易,关键是构造,这里直接采用如果 $w_i=w_{i-1}$,此时就找下一个的下一个(不是下一个的最小的下一个),这里的原始答案默认按照 $0,1,...,n-1$ 的方式来。
-
如果 $w_i\not= w_{i-1}$ ,那么就取 $w_{i-1}$,可以很容易证明这样一定是最优的。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
FOR(i,1,n){
if(w[i]>i){
cout<<-1<<endl;
return;
}
}
FOR(i,1,n){
vis[w[i]]=1;
}
if(w[1]==1)vis[0]=1;
int p=0;
FOR(i,1,n){
if(w[i]==w[i-1]){
while(p<=n+1&&vis[p])p++;
vis[p]=1;
cout<<p<<' ';
}else{
cout<<w[i-1]<<' ';
}
}
}
mex的性质得多掌握,mex如果是连续的情况下,往往跟上一个mex的差值是1。
1514C-Product 1 Modulo N *1600 构造+数学
-
设乘积为 $s$,那么 $s\equiv 1 \ (\mod n)$。
-
此时可以设除 $a_i$ 外其他数的乘积是 $p$,那么:$a_ip\equiv 1\ (\mod n)$,根据裴蜀定理,此时 $gcd(a_i,n)=1$。
-
之后把所有与 $n$ 互质的数在模 $n$ 意义下乘起来,可以得到一个小于 $n$ 的数 $ans$ , $ans$ 一定与 $n$ 互质。(辗转相除的原理)
void solve() {
cin>>n;
int s=1;
FOR(i,1,n-1){
if(__gcd(i,n)==1){
w[i]=1;
s=s*i%n;
}
}
if(s!=1)w[s]=0;
int cnt=0;
FOR(i,1,n-1){
if(w[i])cnt++;
}
cout<<cnt<<endl;
FOR(i,1,n){
if(w[i])cout<<i<<' ';
}
}
最关键的就是得知道裴蜀定理的应用。
1548B- Integers Have Friends *1800 数学
-
要让 $a_i \mod m =a_{i+1} \mod m=...a_j \mod m$,此时很容易知道,对于相邻两项,有:$|a_i-a_{i+1}| \mod m=0$,然后前面的式子有 $j-i$ 个,此时要让所有数 $\mod m=0$,就是得让 $\gcd(|a_i-a_{i+1}|,|a_{i+1}-a_{i+2}|,...)>1$。
-
那个 $\gcd$ 的值是自定义的并且是寻找最长的区间使得 $\gcd>1$,因此可以采用取尺法。
-
区间的 $\gcd$ 值可以用 st 表维护。
void init(){
for(int j=0;j<=20;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
if(!j)f[i][j]=c[i];
else f[i][j]=__gcd(f[i][j-1],f[i+(1<<j-1)][j-1]);
}
}
}
int query(int l,int r){
int len=log2(r-l+1);
return __gcd(f[l][len],f[r-(1<<len)+1][len]);
}
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i],c[i]=abs(w[i]-w[i-1]);
init();
int mx=0;
for(int l=1,r=1;r<=n;r++){
while(l<r&&query(l+1,r)<=1)l++;
mx=max(mx,r-l+1);
}
cout<<mx<<endl;
}
求最长区间满足某个条件的题目可以考虑取尺法。取尺法本质就是两个同向指针。
尺取法处理是问题是一段连续的区间,区间是单调的。
使用尺取法时应清楚以下四点:
1、 什么情况下能使用尺取法? 2、何时推进区间的端点? 3、如何推进区间的端点? 3、何时结束区间的枚举?
尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步
得到根据其端点得到,那么尺取法便是不可行的。首先,明确题目所需要求解的量之后,区间左右端点一般从最整个数组的起点开始,之后判断区间是否符合条件在根据实际情况变化区间的端点求解答案。
1633D-Make Them Equal *1600 dp+数学
-
对于每个数 $a_i$,有 $a_i+\lfloor \frac{a_i}{x} \rfloor$,又因为数组的数最大也就 $10^3$,因此可以预处理出最小操作次数,即: $i=i+i/j$,这个可以暴力取枚举。
-
对于后面的操作就是直接
01背包,因为每个数可以操作和不操作两种情况,对于01背包也就是选和不选两种情况。 -
特判:如果操作次数大于等于 $a$ 数组的和,此时说明所有的数都可以被遍历到。
void init(){
FOR(i,0,1000)g[i]=1e18;
g[1]=0;
FOR(i,1,1000){
FOR(j,1,i){
int k=i+i/j;
g[k]=min(g[k],g[i]+1);
}
}
}
void solve() {
cin>>n>>k;
int tot=0,tc=0;
FOR(i,1,n)cin>>a[i],tot+=a[i];
FOR(i,1,n)cin>>b[i],tc+=b[i];
if(k>=tot){
cout<<tc<<endl;
return;
}
FOR(i,0,k+1)f[i]=0;
FOR(i,1,n){
FORD(j,g[a[i]],k){
f[j]=max(f[j],f[j-g[a[i]]]+b[i]);
}
}
cout<<f[k]<<endl;
}
数小的时候多考虑预处理和dp,题目中是操作次数有选和不选两种情况。
卡:没有想到去预处理,然后想了非常复杂的贪心,
太菜了
1758D-Range = √Sum *1800 构造
-
可以很容易想到偶数的构造是:$$...,n-2,n-1,n+1,n+2,...$$,这个的和是 $n^2$,而且最大值减最小值就是 $n$。
-
奇数考虑:$$\frac{n}{2}+2,...,n,...,n+3,...,\frac{3n}{2}+3$$。这个的和是 $(n+1)^2$,最大值减最小值是 $(n+1)$。
void solve() {
cin>>n;
if(n&1){
FOR(i,n/2+2,n)cout<<i<<' ';
FOR(i,n+3,n+n/2+3)cout<<i<<' ';
cout<<endl;
}else{
FOR(i,n-n/2,n+n/2){
if(i!=n){
cout<<i<<' ';
}
}
cout<<endl;
}
}
构造的题还是多多举例,有的时候很经常要 奇偶分讨。
1774B-Coloring *1500 数学
-
本题要求构造一个序列,要求连续 $k$ 个数不能重复。
-
可以将每 $k$ 个数看作一组,显然有 $(n + k - 1) / k$ 组,一个数最多只能在每组中出现一次,
因此如果存在 $a_i > (n + k - 1) / k$,则一定无解。 -
最后一组并不一定有 $k$ 个,而是有 $(n - 1) % k + 1$(最后一组不可能为 $0$,特殊处理一下),
这意味着只有 $(n - 1) % k + 1$ 能出现 $(n + k - 1) / k$ 次,因此统计一下出现 $(n + k - 1) / k$
次的数的个数 $cnt$,如果 $cnt > (n - 1) % k + 1$,则也一定无解。
void solve() {
cin>>n>>m>>k;
FOR(i,1,m)cin>>w[i];
int t=(n+k-1)/k;
int ans=1e18;
int cnt=0;
FOR(i,1,m){
if(w[i]>t){
cout<<"NO"<<endl;
return;
}else if(w[i]==t){
cnt++;
}
}
if(cnt<=(n-1)%k+1)cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
没有想到平均分组,当时想的太混乱了。如果想到平均分组的话,那这道题就很简单了。
qwq
1774D-Same Count One *1600 思维
-
就是先把横向统计一下,然后看纵向的 $1$ 的个数,这里有维护每一行的 $1$ 的个数。
-
特判比较容易,这里就不强调了。
-
然后就把某一列多余平均的给少于平均的,然后更新一下它们的值。
void solve() {
cin>>n>>m;
vector<vector<int>>w(n+1,vector<int>(m+1,0));
vector<int>col(n+1,0);
int tot=0;
FOR(i,1,n){
int s=0;
FOR(j,1,m){
cin>>w[i][j];
if(w[i][j]){
tot++;
s++;
}
}
col[i]=s;
}
if(tot%n!=0){
cout<<-1<<endl;
return;
}
tot/=n;
VI zero,one;
vector<array<int,3>>res;
FOR(i,1,m){
one.clear(),zero.clear();
FOR(j,1,n){
if(col[j]>tot&&w[j][i]==1)one.pb(j);
if(col[j]<tot&&w[j][i]==0)zero.pb(j);
}
FOR(j,0,min(one.sz,zero.sz)-1){
res.pb({zero[j],one[j],i});
col[one[j]]--;
col[zero[j]]++;
}
}
cout<<res.sz<<endl;
for(auto [x,y,z]:res){
cout<<x<<' '<<y<<' '<<z<<endl;
}
}
这就是很单纯的贪心,个人已经想到预处理每一行 $1$ 的个数,然后肯定跟平均值有关,然后肯定是从多余平均的转移到少于平均的。然后就不想去了。
服了,就差一步。
1776L-Controllers *1500 数学
- 因为给定的数只有两个 $a,b$,因此可以很容易的建立如下方程组:
设 $a$ 的 + 使用次数为 $u$,- 使用次数为 $v$,设使用字符串中的 + 的数量是 $x$, - 的数量是 $y$,那么有:
$$au-av+b(x-u)-b(y-v)=0$$
$$(u−v)⋅(a−b)=−(x−y)⋅b$$
-
接下来是分类讨论一下:
-
- 当 $a-b=0$,,$x−y$ 必为 $0$,否则无解。
-
- 当 $a−b\not=0$ 时,显然 $u−v=a−b−(x−y)⋅b$。根据,$0≤u≤x,0≤v≤y$,通过这两个不等式解得 $−y≤u−v≤x$。
void solve() {
int a,b;
cin>>a>>b;
if(a==b){
if(x==y)cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}else{
int m=x-y;
if(b*m%(b-a)){
cout<<"NO"<<endl;
}else{
int k=b*m/(b-a);
if(k>=-y&&k<=x){
cout<<"YES"<<endl;
}else{
cout<<"NO"<<endl;
}
}
}
}
考的就是方程组,还是解不等式的。
1868B1-Candy Party (Easy Version) *1700 位运算
-
由于题目说什么转移 $2^x$ 颗糖果,此时就得考虑二进制——位运算了。
-
对于每一个数与平均数的差值 $b_i$,可以将它拆成 $2x-2y$ 的形式。
-
如果 $b_i$ 变不成这种形式,那么不可能满足条件——得到和付出 $2^x$ 颗糖果。
-
此时存储满足的 $x$ 和 $y$ 值,如果所有的 $x=y$,那么满足条件。
void solve() {
cin>>n;
int sum=0;
FOR(i,1,n)cin>>w[i],sum+=w[i];
if(sum%n!=0){
cout<<"No"<<endl;
return;
}
sum/=n;
map<int,int> m1,m2;
FOR(i,1,n){
int k=w[i]-sum;
if(!k)continue;//bi=2^x-2^y
int c=abs(k);
int x=log2(c)+1,y=log2((1<<x)-c);
if((1<<x)-(1<<y)!=c){
cout<<"nO"<<endl;
return;
}
if(k<0){
swap(x,y);
}
m1[x]++,m1[y]--;
}
FOR(i,0,32){
if(m1[i]!=0){
cout<<"No"<<endl;
return;
}
}
cout<<"yEs"<<endl;
}
想到了位运算,但卡在:没有把形式很清晰的拆成 $2x-2y$ 的形式来做。
以后遇到类似的话,可以考虑某一个东西(比如差值,变化量等)有一种比较清晰的形式去表示。
1948C-Arrow Path *1300
- 大型
bfs水题。
void bfs(){
queue<PII>q;
q.push({1,1});
while(q.size()){
auto [x,y]=q.front();
q.pop();
for(int i=0;i<4;i++){
int a=dx[i]+x,b=dy[i]+y;
if(a<1||b<1||a>2||b>n)continue;
if(s[a][b]=='>'){
b++;
}else{
b--;
}
if(vis[a][b])continue;
vis[a][b]=1;
q.push({a,b});
}
}
}
void solve() {
cin>>n;
FOR(i,0,2){
FOR(j,1,n){
vis[i][j]=0;
}
}
FOR(i,1,n)cin>>s[1][i];
FOR(i,1,n)cin>>s[2][i];
vis[1][1]=1;
bfs();
cout<<(vis[2][n]?"YES":"NO")<<endl;
}
1948D-Tandem Repeats? *1700 贪心+模拟
- 由于这个数组长度不大,直接暴力枚举就可以了。
void solve() {
cin>>s;
n=s.sz;
s=' '+s;
int l=0,r=n/2;
int ans=0;
FOR(i,1,n/2){
int x=i,mx=0,p=0;
for(int j=1;j+x<=n;j++){
if(s[j]==s[j+x]||s[j]=='?'||s[j+x]=='?'){
p++;
}else{
p=0;
}
mx=max(mx,p);
}
if(mx>=i)ans=max(ans,2*i);
}
cout<<ans<<endl;
}
刚开始还想着二分答案,没想到答案不具有单调性,比如
c?csadasihd,可能后面还能让它更优但二分不下去了。
1954B-Make It Ugly *1200
-
其实总的思路就是从两端往中间靠,就是得注意这种情况:
1 2 2 1,此时答案为0。 -
因此考虑的情况有:
-
- 删除持续删除队尾。
-
- 删除持续删除队头。
-
- 删除中间元素。
-
综上所述,只需要当 $a_i=a_1$ 的时候,再删除。
void solve() {
cin>>n;
map<int,int>S;
FOR(i,1,n)cin>>w[i],S[w[i]]++;
if(S[w[1]]==n){
cout<<"-1"<<endl;
return;
}
int ans=n,len=0;
FOR(i,1,n){
if(w[i]==w[1]){
len++;
}else{
ans=min(ans,len);
len=0;
}
}
ans=min(ans,len);
cout<<ans<<endl;
}
漏了考虑中间元素的删除情况。
1969C-Minimizing the Sum *1700 dp
-
题目说用相邻的元素去替换,那么当把时间范围拉长,此时就是一段区间一段区间的了。
-
由于一个数可以更新多个之相邻的连续的数,最终效果就是这一段的数都变成了这一段的最小值。
-
所以不妨枚举每个段,使之变为最小值,再进行转移。
-
所以设 $f_{i,j}$ 为在前 $i$ 个元素,最多操作 $k$ 次的最小总和。
-
状态转移:$f_{i+l,j+l}=\min(f_{i-1,j}+(l-1)*minv)$。
void solve() {
cin>>n>>K;
FOR(i,0,n){
FOR(j,0,K){
f[i][j]=1e18;
}
}
FOR(i,1,n)cin>>w[i];
f[0][0]=0;
FOR(i,1,n){
FOR(j,0,K){
int minv=1e18;
for(int l=0;l+j<=K&&l+i<=n;l++){//固定长度去枚举最小值
minv=min(minv,w[i+l]);
f[i+l][l+j]=min(f[i+l][l+j],f[i-1][j]+(l+1)*minv);
}
}
}
int ans=1e18;
FOR(i,0,K){
ans=min(ans,f[n][i]);
}
cout<<ans<<endl;
}
是想到dp了,但没有想到优化,就是按照固定长度去枚举最小值放入。而且如果不按照区间的转移,转移方程很难写。(就是卡在对于相邻问题没有考虑区间进行转移)。
1971G-XOUR *1700 构造
-
要让 $a_i \oplus a_j<4$,此时:$\lfloor \frac{a_i}{4}\rfloor = \lfloor \frac{a_j}{4} \rfloor$。
-
也就是:所有除四下取整相同的 $a_i$ 都可以按照原来的位置排序。
-
此时就用小堆来维护 $\lfloor \frac{a_i}{4}\rfloor$ 的值。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
map<int,priority_queue<int,vector<int>,greater<int>>>S;
FOR(i,1,n){
S[w[i]/4].push(w[i]);
}
FOR(i,1,n){
cout<<S[w[i]/4].top()<<' ';
S[w[i]/4].pop();
}
cout<<endl;
}
没有想到 $\lfloor \frac{a_i}{4}\rfloor = \lfloor \frac{a_j}{4} \rfloor$。
1983A-Array Divisibility 水题
- 思路就是直接输出 $1,2,3,...,n$。
void solve() {
cin>>n;
FOR(i,1,n)cout<<i<<' ';
cout<<endl;
}
1983B-Corner Twist *1200 思维
- 首先得发现一个性质:每次变化每行和每列模 $3$ 的值都是不变的。
void solve() {
cin>>n>>m;
FOR(i,1,n){
FOR(j,1,m){
char c;
cin>>c;
s[i][j]=c-'0';
sa[i]=0,ta[i]=0;
tb[j]=0,sb[j]=0;
}
}
FOR(i,1,n){
FOR(j,1,m){
char c;
cin>>c;
t[i][j]=c-'0';
}
}
FOR(i,1,n){
FOR(j,1,m){
sa[i]=(sa[i]+s[i][j])%3;
ta[i]=(ta[i]+t[i][j])%3;
}
}
FOR(i,1,m){
FOR(j,1,n){
sb[i]=(sb[i]+s[j][i])%3;
tb[i]=(tb[i]+t[j][i])%3;
}
}
FOR(i,1,n){
if(sa[i]!=ta[i]){
cout<<"No"<<endl;
return;
}
}
FOR(i,1,m){
if(sb[i]!=tb[i]){
cout<<"No"<<endl;
return;
}
}
cout<<"Yes"<<endl;
}
1983C-Have Your Cake and Eat It Too *1400 双指针
-
就直接枚举每一种情况,然后如果有这种情况,就直接输出即可。
-
枚举的每一种情况就是用双指针维护。
int f(int a[],int b[],int c[],int x,int y,int z,int tot){
vector<PII>res(4);
int l=1,r=n;
int sa=0,sb=0,sc=0;
while(sa<tot)sa+=a[l++];
while(sc<tot)sc+=c[r--];
FOR(i,l,r){
sb+=b[i];
}
// cout<<l<<' '<<r<<' '<<sa<<' '<<sb<<' '<<sc<<endl;
if(sb<tot)return 0;
res[1]={1,l-1};
res[2]={l,r};
res[3]={r+1,n};
id[x]=1,id[y]=2,id[z]=3;
cout<<res[id[1]].fi<<' '<<res[id[1]].se<<' ';
cout<<res[id[2]].fi<<' '<<res[id[2]].se<<' ';
cout<<res[id[3]].fi<<' '<<res[id[3]].se<<endl;
return 1;
}
void solve() {
cin>>n;
int s=0;
FOR(i,1,n)cin>>a[i],s+=a[i];
FOR(i,1,n)cin>>b[i];
FOR(i,1,n)cin>>c[i];
s=(s+2)/3;
if(f(a,b,c,1,2,3,s))return;
if(f(a,c,b,1,3,2,s))return;
if(f(b,a,c,2,1,3,s))return;
if(f(b,c,a,2,3,1,s))return;
if(f(c,a,b,3,1,2,s))return;
if(f(c,b,a,3,2,1,s))return;
cout<<-1<<endl;
}
错误原因:没有正确处理好对应关系,就是
f函数里面的对应关系。
以后就多考虑,自己先写认真考虑一遍再写。
2001A-Make All Equal 水题
- 不说了。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
map<int,int>S;
FOR(i,1,n){
S[w[i]]++;
}
int t=0;
for(auto [x,y]:S){
t=max(t,y);
}
cout<<n-t<<endl;
}
2001B-Generate Permutation 水题
- 可以很容易发现规律就是:如果是偶数就不行,奇数的话按照这种输出:$\lceil \frac{n}{2} \rceil+1,...,n,1,\lceil \frac{n}{2} \rceil$。
void solve() {
cin>>n;
if(n==1){
cout<<1<<endl;
return;
}
if(!(n&1)){
cout<<-1<<endl;
}else{
int t=(n+1)/2;
FORD(i,t+1,n)cout<<i<<' ';
FOR(i,1,t)cout<<i<<' ';
cout<<endl;
}
}
2001C-Guess The Tree *1500 交互题+分治思想
-
本质上就是先从 $1$ 开始猜,如果这里有边的话,就递归 $a,mid$ 和 $mid,b$。
-
这样的时间复杂度是优秀的。
递归的总思路:来源
总的思路:每次询问树根与一个未被标记的点 。然后递归处理:
先把询问的 b 点标记,并得到中点 x。现在需要处理 x 到 b 的路径和 a 到 x 的路径。
对于前者,直接递归下去即可。
对于后者,分类讨论一下:若 x 已被标记,说明 x 到根上的路径已经被确定,不需要处理;如果未被标记,则递归处理 a 到 x 的路径。
递归的终止条件为询问的点 a 与得到的点 x 相同(因为这时询问的 a,b 两点相邻),此时要把 a,b 连边。
int query(int a,int b){
int n;
cout<<"? "<<a<<' '<<b<<endl;
cin>>n;
return n;
}
void dfs(int a,int b){//分治思想
vis[b]=1;
int mid=query(a,b);
if(mid==a){
ans.push_back({a,b});
return;
}
if(!vis[mid])dfs(a,mid);
dfs(mid,b);
}
void solve() {
cin>>n;
FOR(i,2,n){
if(!vis[i]){
dfs(1,i);
}
}
cout<<"! ";
for(auto [x,y]:ans){
cout<<x<<' '<<y<<' ';
}
cout<<endl;
FOR(i,1,n){
vis[i]=0;
}
ans.clear();
}
没有想到用分治去优化时间复杂度,如果按照暴力的话时间复杂度是 $O(n^2)$,如果是分治的话,时间复杂度可以降到 $O(n\sqrt n)$。
2005B2-The Strict Teacher (Hard Version) *1200 水题二分
这道题是我最开始刷codeforces的比赛题目,我记得当时我连这种题目做的都很困难,现在看来我当时是真傻。
- 不说了。
void solve() {
cin>>n>>m>>q;
FOR(i,1,m)cin>>b[i];
sort(b+1,b+1+m);
while(q--){
int x;
cin>>x;
int lt=upper_bound(b+1,b+1+m,x)-b-1;
int rt=lt+1;
if(!lt){
cout<<max<int>(0,b[1]-1)<<endl;
}else if(rt>m){
cout<<max<int>(0,n-b[lt])<<endl;
}else{
int t=(b[lt]+b[rt])/2;
cout<<min(abs(b[lt]-t),abs(b[rt]-t))<<endl;
}
}
}
2032C-Trinity *1400 双指针+思维
-
这道题就先排序,因为排序不会影响答案的个数。
-
其次把问题转化,题目问三角形数组的个数,此时就转化成总的个数减去不满足三角形数组的个数。
-
不满足三角形个数的可以直接用双指针维护。
-
为什么可以呢?因为不满足的个数有两个指针就是相邻的,这样可以把三维的变量转化成二维的变量,然后二维的变量就可以用双指针算法去解决。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
sort(w+1,w+1+n);
int l=1,r=2;
int ans=0;
while(r<=n){
while(l<=n&&w[l]+w[l+1]<=w[r])l++;
ans=max(ans,r-l+1);
r++;
}
cout<<n-ans<<endl;
}
三维变量转化为二位变量可以采取:CDQ分治、排序、转化题意(变成双指针形式)。
Gym-102992F Fireworks 三分+概率
-
假设一个 $E(x)$,表示多次造 $k$ 个烟花后燃放,出现至少一个完美烟花的概率最大的最小期望,简称最优。
-
形象的讲(不保证正确,但是好理解):每次都造 $k$ 个后燃放出现完美烟花的概率是最大的,所以每次我们都只造 $k$ 个烟花后燃放。
-
因此:
$$E(x)=(k×n+m)×(1−(1−p)k)+(k×n+m)×(1−p)k+E(x)×(1−p)^k$$
- 化简:
$$E(x)=\frac{1−(1−p)^k}{k×n+m}$$
- 求导可以看出该函数是个具有波谷的函数,用三分找到 $k$ 次就可求出 $E(x)$。
double EK(int x){
return (n*x+m)/(1-pow((1-p),x));
}
void solve() {
cin>>n>>m>>p;
p=p/1e4;
double l=1,r=1e9;
while((r-l)>eps){
double midl=l+(r-l)/3;
double midr=r-(r-l)/3;
if(EK(midl)>EK(midr))l=midl;
else r=midr;
}
printf("%.10lf\n",EK(r));
}
具有波谷或波峰的函数可以采用三分来维护。
这几天总结与评价:刷题还是得看重质量,往后还是按照 $+100$ rating $2-5$ 题的模式做,最重要的是认真总结。接下来大概花2-3天去把我之前把牛客的专题的题给总结了。
2.15-2.17
妈的,怎么又感冒了,服了,才好几天哪。
abc190-f Shift and Inversions 逆序对;好题
-
如果没有移动的话,就是经典的逆序对,逆序对的求法有分治法和树状数组法。
-
如果有移动操作,那么逆序对的变化量就是 $n-w_i-(w_i-1)$,其中 $n-w_i$ 是 $w_i$ 移到第一位产生新的逆序对数,$w_i-1$ 是 $w_i$ 去掉损失的逆序对数。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i],w[i]++;
int res=0;
FOR(i,1,n){
res+=i-1-query(w[i]);
add(w[i],1);
}
cout<<res<<endl;
FOR(i,1,n-1){
res=res+(n-w[i])-w[i]+1;//把数字移动到前面逆序对变化量
//n-w[i] 为增加逆序对的个数(比该数大的到该数前面,形成逆序对)
//w[i]-1 为减少逆序对的个数(比该数到该数小的后面,拆散逆序对)
cout<<res<<endl;
}
}
逆序对的应用题目,数字移动到端点逆序对的变化。
1714D-Color with Occurrences *1600 dp
-
因为这里的数都是在 $100$ 以内,因此可以用 dp 来解决,那么此时状态表示很容易:$f_{i}$ 表示已经匹配好了前 $i$ 个字母需要花费的最小步数。
-
状态转移:$f_i=\min(f_{i-|s|}+1)$。
-
这里的匹配直接用
strsub来做即可。(因为字符串不长) -
然后要统计方案数,那就在转移的时候记录这个状态是从哪里来的,最后倒着遍历即可。(一般求方案都是这样)
void solve() {
cin>>s;
n=s.sz;
s=' '+s;
cin>>m;
FOR(i,1,m)cin>>p[i];
FOR(i,0,n)ans[i]={-1,-1};
VI f(n+2,1e18);
f[0]=0;
FOR(i,1,n){
FOR(j,1,m){
if(i>=p[j].sz){
if(p[j]==s.substr(i-p[j].sz+1,p[j].sz)){
FOR(k,i-p[j].sz,i-1){
if(f[i]>f[k]+1){
f[i]=f[k]+1;
ans[i]={k,j};
}
}
}
}
}
}
if(f[n]==1e18){
f[n]=-1;
}
cout<<f[n]<<endl;
int x=n;
while(ans[x].first!=-1){
cout<<ans[x].second<<' '<<x-p[ans[x].second].sz+1<<endl;
x=ans[x].fi;
}
}
以后遇到数比较小的时候又不至于太小的时候可以考虑dp来做。(非常重要)
1848C- Binary String Copying *1600 找规律
- 首先发现:$$00110011$$
如果选区间 $[1,8]$,那么它的操作结果其实跟 $[3,6]$ 是一样的。换句话说,就是:对区间 $[1,8]$ 排序也就是对 $[3,6]$ 排序。
-
所以可以记录下每一个 $0$ 右边第一个 $1$ 的位置 $L$ 和每一个 $1$ 左边第一个 $0$ 的位置 $R$,那么对区间 $[l,r]$ 排序就相当于对区间 $[L,R]$ 排序。对于一组数据,最终的答案就是不同的 $[L,R]$ 的数量。
-
只不过就是得注意,如果一个区间的 $0$ 右边的第一个 $1$ 的位置是大于 $1$ 左边第一个 $0$ 的位置,此时说明无需排序。
void solve() {
cin>>n>>q>>s;
s=' '+s;
S.clear();
int pos=0;
FOR(i,1,n){
if(s[i]=='0')pos=i;
L[i]=pos;
}
pos=n+1;
FORD(i,1,n){
if(s[i]=='1')pos=i;
R[i]=pos;
}
int ans=0;
while(q--){
int x,y;
cin>>x>>y;
x=R[x];
y=L[y];
if(x>y)x=1,y=1;//无需排序
if(!S[make_pair(x,y)]){
S[make_pair(x,y)]=1;
ans++;
}
}
cout<<ans<<endl;
}
这种题就是得去发现规律,发现什么是变的什么是不变的,然后发现二者的关系。所以这种低于 $2000$ 的题得多发现规律。
这道题很容易发现只有 $1$ 在前 $0$ 在后才会产生新的串。
1857F-Sum and Product *1600 数学;水题
-
当你看到 $a_i\times a_j=y$ 和 $a_i+a_j=x$,你肯定会想到韦达定理。
-
那么 $a_i,a_j$ 就是 $a^2-xa+y=0$ 的两个解。
-
然后根据求根公式算出答案就可以了。只不过这边要把所有数的个数放入桶中,然后从桶中取出来算方案数(组合数知识)。
void solve() {
cin>>n;
map<int,int>mp;
FOR(i,1,n)cin>>w[i],mp[w[i]]++;
cin>>q;
while(q--){
int x,y;
cin>>x>>y;
int t=x*x-4*y;
if(t<0){
cout<<"0 ";
continue;
}
if((int)sqrt(t)*(int)sqrt(t)!=t){
cout<<"0 ";
continue;
}
if(t==0){
if(x%2==0){
int p=x/2;
cout<<(mp[p]*(mp[p]-1)/2)<<' ';
}else{
cout<<"0 ";
}
continue;
}
int x1=(x-sqrt(t))/2;
int x2=(x+sqrt(t))/2;
cout<<mp[x1]*mp[x2]<<' ';
}
cout<<endl;
}
1867D-Cyclic Operations *1800 找规律,拓扑排序
-
根据 $a_{l_i}$ 改变为 $l_{(i%k)+1}$,当 $k=1$ 时,就是:$a_{l_i}$ 改变为 $l_{1}$,此时就是说明 $a_1=1,a_2=2$ 这样的。
-
当 $k=1$ 的时候类似自环,那么此时当 $k>=2$ 时,能否类比 $k=1$ 的时候呢?
-
发现可以的,此时把 $i\rightarrow w_i$,如果环的大小为 $k$,那么满足条件。

-
那么只要缩点,然后看缩点的联通块的大小即可。
-
这里得注意:
targin是算不了自环的情况,这个得单独判断。
void solve() {
cin>>n>>k;
VII g(n+1);
FOR(i,1,n){
cin>>w[i];
g[i].emplace_back(w[i]);
}
if(k==1){
FOR(i,1,n){
if(i!=w[i]){
cout<<"NO"<<endl;
return;
}
}
cout<<"YES"<<endl;
return;
}
FOR(i,1,n){//注意targin无法统计自环的
if(i==w[i]){
cout<<"NO"<<endl;
return;
}
}
VI dfn(n+1,0),low(n+1,0);
int tmd=0,top=0,scc_cnt=0;
VI scc_size(n+1,0),stk(n+1,0),in_stk(n+1,0);
function<void(int)> targin=[&](int u){
dfn[u]=low[u]=++tmd;
stk[++top]=u,in_stk[u]=true;
for(auto j:g[u]){
if(!dfn[j]){
targin(j);
low[u]=min(low[u],low[j]);
}else if(in_stk[j]){
low[u]=min(low[u],dfn[j]);
}
}
if(low[u]==dfn[u]){
int y;
scc_cnt++;
do{
y=stk[top--];
in_stk[y]=false;
scc_size[scc_cnt]++;
}while(y!=u);
}
};
FOR(i,1,n){
if(!dfn[i]){
targin(i);
}
}
bool f=false;
FOR(i,1,scc_cnt){
if(scc_size[i]!=1&&scc_size[i]!=k){
cout<<"NO"<<endl;
return;
}
if(scc_size[i]==k)f=1;
}
cout<<(f?"YES":"NO")<<endl;
}
有的时候规律都是从特殊情况类比到一般情况的。
1870D-Prefix Purchase *1800 贪心;好题
-
可以很容易有个贪心策略:价格越便宜的多买;尽量买越靠后的。
-
也就是:如果满足 $a_i>a_{i+1}$ 的话,那么选 $i+1$ 位置一定比选 $i$ 位置更优。所以我们就可以先将 $a_i$ 赋值为 $\min(a_i,a_{i+1})$,这样就可以避免掉上面所说的这种情况了。
-
题目要求字典序最大,所以我们肯定要贪心的去先选前面的。但是我们会发现选完 $a_1$ 之后 $k$ 还会剩下一些,如果直接不管的话肯定不是最优的。所以我们就可以用 $k$ 剩下的这些值去将一些选 $a_1$ 的变成选 $a_2$ 的,这样既可以保证当前的字典序不变(选 $1$ 的个数没变),还可以让后面的字典序变大(选 $2$ 的个数变多)。
-
那么就可以算出选 $a_2$ 的数量就应该是:$(k−k÷a1×a1)÷(a2−a1)$。意思是用 $k$ 剩下的值除以每一次变化需要的代价。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
cin>>k;
VI b(n+1,0),ans(n+1,1e18);
int mi=1e18;
FORD(i,1,n){
mi=min(mi,w[i]);
b[i]=mi;
}
FOR(i,1,n){
if(b[i]==b[i-1])ans[i]=ans[i-1];
else ans[i]=min(k/(b[i]-b[i-1]),ans[i-1]);
k-=(b[i]-b[i-1])*ans[i];
cout<<ans[i]<<' ';
}
cout<<endl;
}
卡在第二步,没有想到居然可以倒着维护最小值来解决最优解问题(意思就是价格越便宜的多买,尽量买越靠后的),这样倒着求最小值就能让价格便宜的靠后了。
1875D-Jellyfish and Mex *1600 dp
-
看到数据规模比较小,考虑dp,特别注意是 $n$ 比较小,那么也就说 $mex$ 值不大,因此 dp 只能 dp $mex$ 值。
-
首先状态表示:$f_i$ 表示当前的 $mex$ 值 是 $i$ 的 $m$ 的最小值。
-
状态转移:$f_i=min(f_{j}+(c_i-1)*j+i)$,其中 $j>i$。
-
答案就是 $f_0$。
void solve() {
cin>>n;
map<int,int>cnt;
FOR(i,1,n)cin>>w[i],cnt[w[i]]++;
int mex=0;
while(cnt[mex])mex++;
VI f(mex+1,1e18);
f[mex]=0;
FORD(i,1,mex){
FOR(j,0,i-1){
f[j]=min(f[j],f[i]+(cnt[j]-1)*i+j);
}
}
cout<<f[0]<<endl;
}
没想到居然是用 $mex$ 来 dp 的,服了。
1879D-Sum of XOR Functions *1700 套路题;位运算
- 上公式:
$$\sum_{l=1}{n}\sum_{r=l}f(l,r)\times(r-l+1)$$
$$=\sum_{r=1}{n}\sum_{l=1}f(l,r)\times(r-l+1)$$
$$=\sum_{r=1}{n}\sum_{l=1}\sum_{i=0}^{32} s_{l,i} \operatorname{xor} s_{r,i}\times 2^i\times(r-l+1)$$
$$=\sum_{r=1}{n}\sum_{i=0} \sum_{l=1}^{r}[ s_{r,i}\operatorname{xor}s_{l,i} =1]\times 2^i\times(r-l+1)$$
$$=\sum_{r=1}{n}\sum_{i=0} 2i\times\sum_{l=1}[ s_{r,i}\operatorname{xor}s_{l,i} =1]\times r-[ s_{r,i}\operatorname{xor}s_{l,i} =1] \times (l-1)$$
-
此时把 $r$ 当作变量,后面的 $\sum_{l=1}^{r}[ s_{r,i}\operatorname{xor}s_{l,i} =1]\times r-[ s_{r,i}\operatorname{xor}s_{l,i} =1] \times (l-1)$ 可以直接算出来的,给它一个含义:前缀中与 $s_r$ 值相反的数的个数。
-
注意这里是 $l-1$,因此它的增幅会慢一拍。
-
在代码中体现就是:
cnt[i][j]是中括号满足条件的,sum[i][j]就是 $l-1$ 那一项的。
void solve() {
cin>>n;
FOR(i,1,n){
cin>>w[i];
s[i]=s[i-1]^w[i];
FORD(j,0,32){
sep[i][j]=((s[i]>>j)&1);
}
}
FOR(i,0,32)cnt[i][0]=1;
int ans=0;
FOR(i,1,n){//固定r
FORD(j,0,32){//sep[i][j]^1就是与当前不同的前缀,这样能满足
ans=(ans+(cnt[j][sep[i][j]^1]*i%mod-sum[j][sep[i][j]^1]%mod+2*mod)*(1<<j))%mod;
}
FORD(j,0,32){//r扩展,个数也得扩展,因为是l-1,所以会慢一拍
sum[j][sep[i][j]]+=i;
cnt[j][sep[i][j]]++;
}
}
cout<<ans;
}
公式推导牢记。
1889B-Doremy's Connecting Plan *1700 排序,思维
-
要求 $\sum_{k \in S}a_k \ge i \cdot j\cdot c$ ,此时公式里面有三个变量,不好搞,此时就让 $i=1$,这样就能优化掉一个变量。
-
为什么选 $1$ 可以呢?
证明:假设它们都不能和 1 连边,那么:
$$\begin{aligned}
a_i + a_j \ge i \cdot j\cdot c \
a_i + a_1 < i \cdot c \
a_j + a_1 < j \cdot c
\end{aligned}$$
后两行相加与第一行比较,又由于 $a_1≥0$,得出:
$$(i + j) \cdot c \gt i\cdot j \cdot c$$
$$i + j \gt i\cdot j$$
那么显然这个式子是矛盾的,因此必须跟 $1$ 相连。
- 那么只需要考虑和 $1$ 连边的情况了,此时只需要算:$a_i - i \cdot c$ 即可。此时就按这个从大到小排序,只要 $sum+a_i<i\cdot c$ 就输出
NO。
bool operator<(const E& a,const E& b){
return a.x-c*a.y>b.x-c*b.y;
}
void solve() {
cin>>n>>c;
FOR(i,1,n)cin>>w[i].x,w[i].y=i;
sort(w+2,w+1+n);
int sum=w[1].x;
FOR(i,2,n){
if(sum+w[i].x<w[i].y*c){
No;
return;
}
sum+=w[i].x;
}
Yes;
}
当出现多个变量,可以考虑去掉一个变量的方法去做。
1923D-Slimes *1800 二分;细节题
-
看某个点是否能被史莱姆吃掉,就看它前面或者后面是否能累加起来(必须只有严格大的才能累加)比当前大,因此这里可以用二分来判断存不存在以及最小值。
-
由于题目说一个史莱姆只能在严格大于其邻居的情况下吃掉该邻居,因此我这里处理相等情况以及前缀和。
-
然后就是愉快的二分过程了。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i],pre[i]=pre[i-1]+w[i],c[i]=c[i-1]+abs(w[i]-w[i-1]);
int l,r;
FOR(i,1,n){
if((w[i-1]>w[i]&&i>1)||(w[i+1]>w[i]&&i<n)){
cout<<"1 ";
continue;
}
int ans=1e18;
if(i-1&&pre[i-1]>w[i]){
l=0,r=i;
while(l+1!=r){
int mid=l+r>>1;
if(pre[i-1]-pre[mid-1]<=w[i]||(c[i-1]-c[mid]==0&&i-1!=mid))r=mid;
else l=mid;
}
if(l!=0){
ans=min(ans,i-l);
}
}
if(i-n&&pre[n]-pre[i]>w[i]){
l=i,r=n+1;
while(l+1!=r){
int mid=l+r>>1;
if(pre[mid]-pre[i]<=w[i]||(c[mid]-c[i+1]==0&&i+1!=mid))l=mid;
else r=mid;
}
if(r!=n+1){
ans=min(ans,r-i);
}
}
cout<<(ans==1e18?-1:ans)<<' ';
}
cout<<endl;
}
二分思路其实很简单的,因为你要找最小值,那么此时二分一个答案,看看能不能满足,不能满足就继续二分,具备二段性和单调性。
但这道题的细节特别多,一不小心就错了。
1934C-Find a Mine *1700 猜答案
-
因为只有 $4$ 次查询机会,保证网格中恰好有 $2$ 个地雷。还可以发现对于一个地雷,它产生的最小曼哈顿距离在同一个对角线上都是相同的。
-
第一次查询:可以先查询 $(1,1)$ 这个点,把距离记为 $d_1$。
-
第二次查询和第三次查询:根据 $d_1$ 的值跟 $n$ 和 $m$ 进行比较,进而将坐标推进到对角线上。
-
第四次查询:现在已经查找到了对角线的两个端点 $(x_1,y_1)$ 和 $(x_2,y_2)$,此时根据 $d_2$ 和 $d_3$ 定位到一个地雷的位置,如果这个位置得到的 $d_4$ 是 $0$ 的话,那么就是答案了。
图:

void solve() {
cin>>n>>m;
cout<<"? 1 1"<<endl;
cin>>d[0];
int x1,y1,x2,y2;
if(d[0]<n){
x1=d[0]+1,y1=1;
}else{
x1=n,y1=d[0]+2-n;
}
if(d[0]<m)x2=1,y2=d[0]+1;
else x2=d[0]+2-m,y2=m;
cout<<"? "<<x1<<' '<<y1<<endl;
cin>>d[1];
cout<<"? "<<x2<<' '<<y2<<endl;
cin>>d[2];
d[1]/=2,d[2]/=2;
x1-=d[1],y1+=d[1];
x2+=d[2],y2-=d[2];
cout<<"? "<<x1<<' '<<y1<<endl;
cin>>d[3];
if(!d[3])cout<<"! "<<x1<<' '<<y1<<endl;
else cout<<"! "<<x2<<' '<<y2<<endl;
}
卡在:已经知道第一次查询,然后也知道扩展坐标到对角线的端点,但我无法确定第二次查询和第三次查询该如何操作。
又结束咯,题目不在于多,而在于精。
这个后期改为好题归类,因为计划变更为:每天得VP一场古老cf。相信自己能变得越来越强吧
2.17-3.4
已经好久都没有更新了,这次准备大更新。
580B-Kefa and Company *1500 尺取法
-
题意:给定 $n$ 个人的金额和友谊因素,当选的人的钱 $<d$ 的情况下,那么可以选他,问最后选的人的最大总友谊因素和。
-
思路:因为题目判定是否为朋友的条件是钱 $<d$,那么我们可以先对钱进行排序,然后我们采用 尺取法,在尺子内的朋友都是满足条件的,我们求一下它们的友谊因素和,取个最大值。
int n,m;
struct E{
int a,b;
bool operator<(const E &x) const {
return a < x.a;
}
}w[N];
int s[N];
void solve() {
cin>>n>>m;
FOR(i,1,n){
cin>>w[i].a>>w[i].b;
}
sort(w+1,w+n+1);
FOR(i,1,n){
s[i]=s[i-1]+w[i].b;
}
int ans=0;
int r=0;
FOR(l,1,n){
while(r<=n&&w[r].a-w[l].a<m){
ans=max(ans,s[r]-s[l-1]);
r++;
}
}
cout<<ans<<endl;
}
616D-Longest k-Good Segment *1600 尺取法
-
题意:给定一个包含 $n$ 个整数的数组 $a$。我们将数组中一个或多个连续元素的序列称为 $a$ 的 段。同时,如果一个段包含的不同值不超过 $k$ 个,则称该段为 $k-好$。找到任意最长的 $k-好$段。
-
思路:对于这种找到某一段的xxx性质,此时都得考虑尺取法。它说什么不同元素的,此时这个可以用桶来做。此后,就是尺子内的元素都满足有 $k$ 个不同的元素。
int n,k;
int w[N];
struct E{
int l,r,len;
bool operator<(const E& t)const{
return len>t.len;
}
};
void solve() {
cin>>n>>k;
FOR(i,1,n)cin>>w[i];
vector<E>ans;
map<int,int>S;
int r=0;
FOR(l,1,n){
while(r<n&&S.sz<=k){
S[w[++r]]++;
}
if(S.sz<=k+1&&r<n){
ans.pb({l,r-1,r-l});
}else if(r==n&&S.sz<=k){
ans.pb({l,r,r-l+1});
}
if(--S[w[l]]==0){
S.erase(w[l]);
}
}
sort(ALL(ans));
cout<<ans[0].l<<" "<<ans[0].r<<endl;
}
702C-Cellular Network *1500 二分+尺取法
-
题意:给定一条直线上的 $n$ 个村庄和 $m$ 个基站,现在要找出最小的 $r$,使得每个城市都被移动通信网络覆盖。
-
思路:我们发现这道题有二段性,就是当 $r$ 比较小肯定不满足,$r$ 比较大肯定满足,此时就会有一个分界线,因此满足二分答案条件。那么现在已知 $r$,要看是否能覆盖所有村庄,此时我们可以用尺取法把能覆盖的村庄都标记一下,然后最后遍历所有村庄看哪些村庄没有被标记。
int n,m;
int a[N],b[N];
bool check(int x){
VI vis(n+1,0);
int j=1;
FOR(i,1,n){
while(j<=m&&abs(a[i]-b[j])>x)j++;
if(j==m+1)break;
if(abs(a[i]-b[j])<=x)vis[i]=1;
}
FOR(i,1,n){
if(vis[i]==0)return false;
}
return true;
}
void solve() {
cin>>n>>m;
FOR(i,1,n)cin>>a[i];
FOR(i,1,m)cin>>b[i];
int l=-1,r=1e12;
while(l+1!=r){
int mid=l+r>>1;
if(check(mid))r=mid;
else l=mid;
}
cout<<r;
}
尺取法用处:解决连续段满足特定性质的个数、起始下标等。
1995C-Squaring *1800 贪心
- 题意:给你一个大小为 $n$ 的数组,你每次可以把数组中的数 $a_i$ 改成 $a_i^2$,问最小的操作次数使得数列非递减。
- 思路:我们可以先考虑最简单的情况,那就是当 $a_i=1$ 的情况,因为 $1$ 不管怎么平方都是等于 $1$,如果 $1$ 在中间那就很不好说了,此时就无解;现在考虑一般情况,如果 $a_{i-1}>a_i$,那么此时我们就可以把 $a_i$ 一直平方直到它彻底比 $a_{i-1}$ 大,此时为了避免每一次都计算,我们可以考虑用dp,也就是设:第 $i$ 个数操作次数为 $f[i]$。然后只需要比较相邻的数就行,因为如果本来这个数就是单调递增的话,此时就不会被算进去了,此时就可以节省计算量。但这里要注意的是:比如
16 2 4 2,这种,因为16 2是递减的,因此我们要让不递减的话,就让2操作 $3$ 次即可,然后就开始处理2 4,我们发现这两个数本来就是单调递增的,此时我们直接延用上一次操作,但直接沿用上一次操作可能会出现的问题就是会平方过头了,所以我们要开根号来避免这种情况。
int n;
int w[N];
void solve() {
cin>>n;
VI f(n+1,0);
FOR(i,1,n)cin>>w[i];
int ans=0;
FOR(i,2,n){
int now=w[i],cnt=0;
if(now<w[i-1]){
if(now==1){
cout<<"-1"<<endl;
return;
}
while(now<w[i-1]){
now=now*now;
cnt++;
}
}else{
while((w[i-1]==1)&&sqrt(now)>w[i-1]||(w[i-1]!=1&&sqrt(now)>=w[i-1])){
now=sqrt(now);
cnt--;
}
}
f[i]=max({f[i],(int)0,f[i-1]+cnt});
}
FOR(i,1,n)ans+=f[i];
cout<<ans<<endl;
}
为了避免重复计算,我们可以考虑沿用上一次操作(类似相对值的操作)
2006C-Action Figures *1500 思维
- 题意:给定一个人每天的出行情况,$1$ 表示去商场购物,$0$ 表示没有。如果他在用一天买了两个以上的玩具,那么他可以免费获得他购买的最贵的玩偶(玩具的价钱和天数 $i$ 有关,且他只能买前 $i$ 天的玩具)。问它买所有玩具需要的最少花费。
- 思路:我们可以发现,我们如果在有 $1$ 的那天出门,此时我们买的都是之前 $0$ 的那几天的玩具。因此我们可以倒叙遍历,把 $1$ 对应的下标都存下来,然后到 $0$ 的时候拿出来,这样也就相当于有 $1$ 的时候我们就买它前面 $0$ 的玩具,注意是尽可能的买,当出现
10001这样的情况,我们就是得把前面的0全部买了。如果我们的 $1$ 有剩的话,我们就搭配最小的和最大的买即可(也就是比如第 $1$ 个和第 $n$ 个买)。这里可以用栈或者堆来维护。
string s;
int n;
void solve() {
cin>>n>>s;
s=' '+s;
deque<int>q;
int ans=0;
FORD(i,1,n){
if(s[i]=='1'){
q.push_back(i);
}else{
if(q.sz)q.pop_front();
ans+=i;
}
}
while(q.sz){
ans+=q.back();
q.pop_back();
if(q.sz==0)break;
q.pop_front();
}
cout<<ans<<endl;
}
一下是被1200的题虐的现场,也不知道怎么回事,我居然1200的题会卡半天。
1979C-Earning on Bets *1200 数学、构造
-
题意:有一种游戏,在这种游戏中,有 $n$ 种可能的结果,对于每一种结果,如果获胜的话,将会得到你在该结果上押注的硬币数量乘以 $k_i$ 的硬币数。你的任务是合理分配好硬币,以便在任何获胜的结果发生时都能获利,获胜的条件是:你在所有结果上押注的硬币总数必须小于每个可能获胜结果所获得的硬币数。求每次押注的硬币。
-
思路:题目也就是要构造一个数组 $b_i$,使得 $a_ib_i>\sum_{j=1}^{n}b_j$。我们此时发现,只要 $a_ib_i$ 都相等时最优。那怎样让它们相等,此时就是算 $a_i$ 的 $lcm$(最小公倍数),但数不能超过 $20$,因为超过 $20$ 的 lcm 就飞天了。
int n,w[N];
int lcm(int x,int y){
return x*y/__gcd(x,y);
}
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
if(n>=20){
cout<<"-1"<<endl;
return;
}
if(n==1){
cout<<1<<endl;
return;
}
int t=lcm(w[1],w[2]);
FOR(i,3,n){
t=lcm(t,w[i]);
}
int ans=0;
FOR(i,1,n){
ans+=t/w[i];
}
if(ans>=t){
cout<<-1<<endl;
return;
}
FOR(i,1,n)cout<<t/w[i]<<" \n"[i==n];
}
我靠,我居然被1200的题干成这样,服了。
1954C-Long Multiplication *1200 数学
-
题意:给定长度相同的数字 $x,y$,数字由 $0$ 到 $9$ 组成,你可以进行以下操作任意次:交换 $x$ 中的第 $i$ 位数字和 $y$ 中的第 $i$ 位数字,最后使得它们的乘积最大。
-
思路:我们可以知道,当两个数的差值越小,它们的乘积越大。因为长度最长也就 $100$,那么我们可以贪心的枚举每一位,然后交换之后看看差值会不会变小。具体来说的话就是:如果 $s>t$ 并且 $s_i>t_i$,此时交换更优。
void solve() {
cin>>s>>t;
int n=s.sz;
FOR(i,0,n-1){
if(s>t&&s[i]>t[i])swap(s[i],t[i]);
else if(s<t&&s[i]<t[i])swap(s[i],t[i]);
}
if(s<t)swap(s,t);
cout<<s<<endl<<t<<endl;
}
1990C-Mad MAD Sum *1500 找规律
-
题意:给出一个初始数组。重复同时将数组的每一位变为从数组到此为止出现至少两次的数中最大者,直到数组元素全为 $0$。求该过程中每一步得到的数组(包括初始数组)元素和。
-
思路:我们可以把每一步列出来,我们会发现它有规律:
| 1 | 1 | 4 | 5 | 1 | 4 | 1 | 9 | 1 | 9 | 8 | 1 | 0 | ||
| 0 | 1 | 1 | 1 | 1 | 4 | 4 | 4 | 4 | 9 | 9 | 9 | 9 | ||
| 0 | 0 | 1 | 1 | 1 | 1 | 4 | 4 | 4 | 4 | 9 | 9 | 9 | ||
| 0 | 0 | 0 | 1 | 1 | 1 | 1 | 4 | 4 | 4 | 4 | 9 | 9 | ||
| 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 4 | 4 | 4 | 4 | 9 |
然后我们计算的时候注意,这样算更方便:对角线 \ 类似这种线来算。还有就是得注意:得操作一次之后才会有这种性质。
int n,w[N],a[N];
void solve() {
cin>>n;
int ans=0;
map<int,int>S;
FOR(i,1,n)cin>>w[i],ans+=w[i];
FOR(i,1,n)a[i]=0;
set<int>P;
int mx=0;
FOR(i,1,n){
if(P.count(w[i])){
mx=max(mx,w[i]);
}
a[i]=mx;
P.insert(w[i]);
}
P.clear();
FOR(i,1,n)ans+=a[i];
fill(w+1,w+1+n,0);
mx=0;
FOR(i,1,n){
if(P.count(a[i])){
mx=max(mx,a[i]);
}
w[i]=mx;
P.insert(a[i]);
}
FOR(i,1,n)ans+=w[i];
FOR(i,1,n)ans+=(n-i)*w[i];
cout<<ans<<endl;
}
1806C-Sequence Master *1600 打表
-
题意:给定长为 $2n$ 的数组 $p$,你需要构造一个长度为 $2n$ 的数组 $q$,满足 $ S \subseteq U $ 且 $ |S|=m $,$ \prod\limits_{i \in S} q_i = \sum\limits_{i \in U \setminus S} q_i $。
且你构造的 $q$ 数组需要让 $ans = \sum\limits_{i=1}^k|a_i-b_i| $ 最小,输出这个 $ans$。 -
思路: 不难构造一个全零数组 $q$,即可满足题目要求。
接下来观察样例,由第一组样例发现 $n=1$ 时容易构造 $q={q_1,q_1}$,即可使答案最小。
由第二组样例发现 $n=2$ 时可以构造 $q={2,2,2,2}$,可能可以使答案最小。
第三组样例发现很难找出什么性质,甚至无法手玩出样例,此时尝试写一个爆搜程序暴力枚举 $q$ 数组,观察样例输出 $5$,$p={-2,-2,2,2}$,所以我们可以尝试将 $p$ 数组的每个元素都加上 $-5 \sim 5$ 来尝试得到 $q$ 数组,当枚举出一个 $q$ 数组时就更新一下答案。
发现最后得到的 $q={-1,-1,-1,2}$,尝试将 $n$ 推广到更大,发现当 $n$ 为偶数时可以构造出 $q={-1,-1,……,-1,n}$,而 $n$ 为奇数时则无法构造。
最后观察第四组样例,进一步确认了 $q={-1,-1,……,-1,n}$ 的正确性。
分讨一下 $n \le 2$ 的情况和 $n$ 的奇偶即可。思路引用
int n,w[N*2];
void solve() {
cin>>n;
FOR(i,1,n*2)cin>>w[i];
sort(w+1,w+1+2*n);
if(n==1){
cout<<w[2]-w[1]<<endl;
return;
}
int ans=1e18;
if(n==2){
int sum=0;
FOR(i,1,2*n){
sum+=abs(w[i]-2);
}
ans=min(ans,sum);
}
if(n%2==0){
int sum=0;
FOR(i,1,2*n-1){
sum+=abs(-1-w[i]);
}
sum+=abs(n-w[n*2]);
ans=min(ans,sum);
}
int sum=0;
FOR(i,1,2*n){
sum+=abs(w[i]);
}
ans=min(ans,sum);
cout<<ans<<endl;
}
1903C-Theofanis' Nightmare *1400 前缀和
- 题意:给你一个数组,现在要求你构造一种分发,把数组分成 $k$ 份,使得 $\sum_{i=1}^{k}i\times sum_i$ 最大,求出这个的最大值。
- 思路:我们有个很明显的思路就是,按照后缀只要一旦大于 $0$ 的,我们就分割,这样肯定是最优的,因为不是大于 $0$ 的分,那么 $sum_i<0$,此时就会使得答案变小。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
VI suf(n+2,0);
FORD(i,1,n){
suf[i]=suf[i+1]+w[i];
}
//
int id=0,ans=0;
VI p;
FORD(i,1,n){
if(suf[i]>0){
p.pb(i);
}
}
if(!p.sz||p.back()!=1){
p.pb(1);
}
int pp=p.sz;
int lst=n+1;
for(auto x:p){
// cout<<x<<endl;
ans+=pp*(suf[x]-suf[lst]);
lst=x;
pp--;
}
cout<<ans<<endl;
}
1909C-Heavy Intervals *1400 贪心
- 题意:给定了长度为 $n$ 的三个序列 $l,r,c$,保证 $l_i<r_i$,并且 ${l_1,l_2,l_3,...,l_n,r_1,r_2,...,r_n}$。你需要重新排列这三个序列,排完序依然保证 $l_i<r_i$。但是你需要最小化 $∑_{i=1}^{n}(r_i−l_i)×c_i$ 的值。
- 思路:因为你要最小化这个值,因此我们可以考虑从小到大排序,然后固定 $r_i$ 来找所有满足 $r_i>l_i$ 的 $l_i$,此时如果长度比较长的话,我们就分配小的 $c_i$ 即可。
void solve() {
cin>>n;
FOR(i,1,n)cin>>l[i];
FOR(i,1,n)cin>>r[i];
FOR(i,1,n)cin>>c[i];
sort(l+1,l+1+n),sort(r+1,r+1+n),sort(c+1,c+1+n);
int j=1;
stack<int>stk;
VI ans(n+1,0);
FOR(i,1,n){
while(r[i]>l[j]&&j<=n){
stk.push(j);
j++;
}
ans[i]=r[i]-l[stk.top()];
stk.pop();
}
sort(ans.begin()+1,ans.end(),GI());
int res=0;
FOR(i,1,n){
res+=(ans[i]*c[i]);
}
cout<<res<<endl;
}
1916C-Training Before the Olympiad *1200 数学+思维
- 题意:有一个正整数序列 $a$,两人进行游戏,规则如下:
- 当序列中只有一个元素时,游戏结束。
- 否则,两人交替操作,每次需要选出 $a$ 中的两个元素 $a_i,a_j(i\ne j)$,将它们删除并插入一个值为 $\lfloor\frac{a_i+a_j}{2}\rfloor\cdot 2$ 的元素。
-
先手希望使得最终 $a$ 中留下的数最大化,后手希望最小化,两人都按最优策略进行游戏,求对于给定的长为 $n$ 的序列 $a$ 的每个非空前缀,对该前缀进行游戏的结果,即最后留下的数。每个测试点 $t$ 组测试用例。
-
思路:我们发现,对于这种下取整,如果分母是奇数的话,那么除 $2$ 下取整再乘 $2$ 得到的结果就会比之前少 $1$。那么根据这个特性,我们就可以很容易按照如下方式构造:因为如果先手要获胜的话,它肯定是最好取两个偶数出来,很明显是吧。然后后手肯定是把偶数都变成奇数更好,所以对于一轮回合来说,就是偶数最好是被取 $3$ 个 $3$ 个的。因为你模三肯定会有剩余的数,所以如果余 $1$ 的话,此时这个数不管被先手拿还是后手拿这个数必然 $-1$。然后题目问每一次操作,那么我们可以用前缀和来维护前缀中的奇数的个数和偶数的个数。
void solve() {
cin>>n;
FOR(i,1,n){
cin>>w[i];
}
VI pre(n+1,0),pt(n+1,0);
FOR(i,1,n){
if(w[i]&1)pre[i]=pre[i-1]+1;
else pre[i]=pre[i-1];
pt[i]=pt[i-1]+w[i];
}
FOR(i,1,n){
if(i==1)cout<<w[1]<<' ';
else{
int t=pre[i];
int p=t/3;
if(t%3==1)p++;
cout<<pt[i]-p<<' ';
}
}
cout<<endl;
}
认真分析题目性质得到:必须得奇偶分析。
1923C-Find B *1400
- 题意:给你一个数组,再给你 $q$ 次查询,判断这个子数组是不是好的,如果是好的必须满足:它能构造出一个长度跟自身相同且构造数组的和跟原数组的和相同并且对于同一个位置的元素值不同。
- 思路:因为题目说两个要是好的当且仅当和是相同的,因此我们可以用前缀和来维护区间的和,然后我们可以这样构造:如果区间中某个数不是 $1$ 的话,我们就分配 $1$,如果是的话,我们就平均分配不是 $1$ 的个数。这样构造是最优的。
void solve() {
cin>>n>>q;
VI pre(n+1,0),pre2(n+1,0);
FOR(i,1,n)cin>>w[i];
int mx=0;
FOR(i,1,n){
pre[i]=pre[i-1]+w[i]-1;
pre2[i]=pre2[i-1]+(w[i]==1);
}
while(q--){
int l,r;
cin>>l>>r;
if(l==r){
NO;
}else{
if(pre[r]-pre[l-1]<pre2[r]-pre2[l-1]){
NO;
}else{
YES;
}
}
}
}
1933D-Turtle Tenacity: Continual Mods *1200 思维
- 题意:给定一个数组 $a$,问是否存在一种把 $a$ 数组排列的方案数组 $b$,使得 $b$ 所有的元素模数不等于零。
- 思路:我们首先考虑特殊情况:所有数都相同,那么此时肯定是NO。我们发现,如果我们从小到大排序数组的话,此时是更优秀的,因为假如没有重复的数的情况下,比如说
1 2 3 4 5,那么最终答案就是1。所以我们就把数组从小到大排序,此时我们又发现如果 $a_1\not= a_2$ 的情况下,最后答案肯定不为 $0$。反之,如果比如这种情况2 2 3,此时如果不调整顺序的话,那么它的值是零。但如果我们把3放在2的前面就可以了。因此对于这种情况的话,就是去找 $a_i % a_1 \not= 0$ 的就行了。
void solve() {
cin>>n;
map<int,int>S;
FOR(i,1,n)cin>>w[i],S[w[i]]++;
int ans=0;
if(S[w[1]]==n){
NO;
return;
}
if(S[1]>1){
NO;
return;
}
sort(w+1,w+n+1);
if(w[1]!=w[2]){
YES;
return;
}
FOR(i,2,n){
if(w[i]%w[1]){
YES;
return;
}
}
NO;
}
思维题得慢慢积淀。
1935B-Informatics in MAC *1200 思维
- 题意:给定一个长度为 $n$ 的数组 $a$,你要把它分成 $k>1$ 段,此时每一段的 MEX 值都相同,求出此时的 MEX 值和方案。
- 思路:根据 MEX 的性质,因为要分成至少两段,那么我们一般采取的策略就是分成两段。那么此时就得分类讨论了。如果数组中没有 $0$,那就随便分,如果存在一个零,则无解,如果存在两个以上的零,此时我们就得维护前缀 MEX 值和后缀 MEX 的值,当它们相等的时候就记录下来此时的分界线即可。
void solve() {
cin>>n;
map<int,int>S;
FOR(i,1,n)cin>>w[i],S[w[i]]++;
if(!S[0]&&n>1){
cout<<2<<endl;
cout<<"1 1"<<endl;
cout<<"2 "<<n<<endl;
return;
}
if(S[0]==1){
cout<<-1<<endl;
return;
}
int id=0;
VI pre(n+2,0),suf(n+2,0);
set<int>P;
int mex1=0,mex2=0;
FOR(i,1,n){
P.insert(w[i]);
while(P.count(mex1))mex1++;
pre[i]=mex1;
}
P.clear();
FORD(i,1,n){
P.insert(w[i]);
while(P.count(mex2))mex2++;
suf[i]=mex2;
}
FOR(i,1,n){
if(pre[i]==suf[i+1]){
id=i;
break;
}
}
if(id){
cout<<2<<endl;
cout<<1<<' '<<id<<endl;
if(id==n){
cout<<n<<' '<<n<<endl;
}else{
cout<<id+1<<' '<<n<<endl;
}
}else{
cout<<-1<<endl;
}
}
1990B-Array Craft *1200 思维
- 题意:构造一个数组(元素只包含 $-1$ 和 $1$)满足它的前缀和等于前缀和的最大值的最小索引,后缀和等于后缀和的最大值的最大索引,并且这个最大值是给了你的。
- 思路:一个初步的想法就是在前缀的最小索引的后面全部变成
-1,然后后缀的前面全部变成-1。但这个只是想到了一半,因为如果是这样的5 4 3,如果按照我们之前的想法答案就是-1 -1 -1 1 -1,那么x=1,它就不会等于4了。所以我们想到了交替的来,就是构造成这样-1 1 -1 1 -1,也就是-1和1交替进行。这样就能理解出题人为什么 $y<x$ 了。
void solve() {
cin>>n>>x>>y;
VI ans(n+1,0);
fill(ALL(ans),1);
FOR(i,x+1,n){
ans[i]=ans[i-1]*-1;
}
FORD(i,1,y-1){
ans[i]=ans[i+1]*-1;
}
FOR(i,1,n)cout<<ans[i]<<" \n"[i==n];
}
有的结论很不好猜,比如这道题你肯定一开始就觉得是从x+1到n都是-1,y-1到1都是-1。
2013C-Password Cracking *1400 交互题
- 题意:猜二进制密码,最多有 $2n$ 次操作,当我们询问一个字符串,它能返回当前字符串是不是答案字符串的子串。
- 思路:题目说最多有 $2n$ 次操作,说明此时我们可以按每次添位数来询问,就是假设当前字符串是 $s$,我们可以在 $s$ 的前面放 $0$ 或者 $1$,或者在它的后面放 $0$ 或者 $1$,这里最多询问 $2$ 次。
int query(string s){
cout<<"? "<<s<<endl;
int res;
cin>>res;
return res;
}
void solve() {
cin>>n;
string s;
bool f=false;
while(1){
if(!f){
int t=query(s+'0');
if(t==1)s+='0';
else{
t=query(s+'1');
if(t){
s+='1';
}else{
f=1;
}
}
}else{
int t=query('0'+s);
if(t)s='0'+s;
else{
s='1'+s;
}
}
if(s.sz==n){
cout<<"! "<<s<<endl;
break;
}
}
}
很多交互题都是看询问次数来确定怎么去猜答案。
2064B-Variety is Discouraged *1100 思维
- 题意:定义数组 $b$ 的贡献为 $b$ 中元素个数减去 $b$ 中不同元素的数量。空数组贡献为 $0$。给出一个数组 $a$。拥有一次对其操作的机会,删除 $a$ 的一个子段。即选择两个整数 $l$ 和 $r$,其中 $1 \le l \le r \le n$,删除连续的子数组 $[a_l,\ldots,a_r]$。也可以不进行操作。如果有多个答案,则输出贡献最大的操作。请最小化 $a$ 的贡献。如果还是有多个答案,输出其中的任何一个。
- 思路:我们发现如果我们删除数组中只出现一次的数,那么此时删掉这个数,答案是不会变化的。如果删除重复的数,此时我们答案会减少 $1$。因此这道题也就是去找出现一次的最长的连续字串的长度。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
VI b(n+1,0);
FOR(i,1,n)b[w[i]]++;
if(b[w[1]]==n&&n>1){
cout<<0<<endl;
return;
}
int ans=0,l=0,r=0;
FOR(i,1,n){
if(b[w[i]]==1){
int j=i;
while(j+1<=n&&b[w[j+1]]==1)j++;
if(j-i+1>ans){
ans=j-i+1;
l=i,r=j;
}
i=j;
}
}
if(!ans){
cout<<0<<endl;
return;
}
cout<<l<<' '<<r<<endl;
}
我怎么感觉 1600 以下的题都是要善于转化的题(思维题)
2064C-Remove the Ends *1300 思维
- 题意:给你一个硬币序列,如果 $a_i<0$,此时获得当前硬币的数量然后把数组后缀删除,反之删除前缀,问在过程结束时你可以拥有的最大硬币数。
- 思路:我们发现,如果 $a_i<0$ 就是删除后缀,反之删除前缀,那么也就是说,我们此时可以维护所有小于零的后缀,然后再维护所有大于零的前缀,然后枚举在某个点结束即可。(可能讲的不清楚,可以看看代码)
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
VI pre(n+n,0),suf(n+n,0);
FOR(i,1,n)if(w[i]>0)pre[i]=pre[i-1]+w[i];else pre[i]=pre[i-1];
FORD(i,1,n)if(w[i]<0)suf[i]=suf[i+1]+w[i];else suf[i]=suf[i+1];
int ans=0;
if(n==1){
cout<<abs(w[1])<<endl;
return;
}
FOR(i,1,n){
ans=max(ans,pre[i]+abs(suf[i]));
}
cout<<ans<<endl;
}
2064D-Eating *1900 位运算
- 题意:给你一个数组,每次询问在初始数组后面加入一个数字,如果倒数第一个数字大于倒数第二个数字,则可以吞噬倒数第二个数字,并把倒数第一个数字更新为二者的异或和,问每次能吞噬初始数组多少个数字。
- 思路:对于
xor的问题,首先按位进行考虑。由于我们能干掉的史莱姆权值都比我们小,所以可以很显然的观察到他们的最高位一定是小于等于我们的,并且我们的最高位在合并史莱姆的过程中只会下降。这启示我们观察到可能使当前结束的点,最高位是大于等于当前值的最高位的。所以我们用 $\text{O}(N\log V)$ 的时间预处理出一个 $pre_{i,j}$ 数组用来存储离当前点 $i$ 最近的最高位大于等于当前值的最高位 $j$ 的下标。而且每次跳转我们要么直接结束,要么最高位至少下降 $1$ 位,所以每次询问的复杂度是 $\text{O}(\log V)$ 的。
void solve() {
cin>>n>>q;
FOR(i,1,n)cin>>w[i];
reverse(w+1,w+1+n);
VI pre(n+2,0);
FOR(i,1,n+1){
pre[i]=pre[i-1]^w[i];
}
VII ne(n+2,VI(30,0));
FOR(i,0,29){
ne[n+1][i]=n+1;
}
FORD(i,1,n){
int x=w[i];
ne[i]=ne[i+1];
FOR(j,0,29){
if(x>>j&1){
ne[i][j]=i;
}
}
}
while(q--){
int x;
cin>>x;
int l=0,r=n+1;
FORD(i,0,29){
if((x^pre[l])>>i&1){
if(r==ne[l+1][i]){
break;
}
if(ne[l+1][i]<r&&(x^pre[ne[l+1][i]-1])>=w[ne[l+1][i]]){
l=ne[l+1][i];
r=min(r,ne[l+1][i]);
}else{
r=min(r,ne[l+1][i]);
break;
}
}else{
r=min(r,max(ne[l+1][i],l+1));
}
}
cout<<r-1<<" \n"[q==0];
}
}
逐步模拟显然会超时,涉及二进制要比大小首先看最高位,最高位一定是单调不递增的,如果最后两个数字最高位相同,那么异或后就会变小,由于最高位最多只会变化 $30$ 次,所以可以从这里考虑,我们要找的其实是当前范围内最右侧的和最后一个数字最高位相同的那个值的位置,这样能跳过中间部分快速更新最后一个位置,可以预处理二进制数最高位的信息,然后再用异或前缀和快速更新最后的值。
这道题等待新的解法更新
abc377d-Many Segments 2 贡献法
- 题意:给出 $N$ 个区间 $[l_i,r_i](1\le i\le N,l_i\le r_i\le M)$,问有多少个区间 $[l,r](1\le l\le r\le M)$ 满足不会包含任意给出区间。
- 思路:此时我们可以先固定一个左端点,然后看 $[l,n]$ 有多少满足不完全包含的条件。此时不包含条件可以变成在区间 $[l,n]$ 中,找到一个 $r$ ,这个 $r$ 很明显肯定就是左端点离 $l$ 最近的线段的右端点减一(这个很明显,建议画图理解)。那么为了研究方便,我们是从大到小枚举 $l$ 的,这样就不会有后效性(也就是如果我们从小到大枚举的话,如果用双指针时间复杂度就高了)。然后我们用双指针维护即可。
void solve() {
cin>>n>>m;
FOR(i,1,n){
int x,y;
cin>>x>>y;
w[i]={x,y};
}
sort(w+1,w+1+n);
int lst=m+1;
int ans=0;
int j=n;
FORD(i,1,m){
while(j>=1&&w[j].fi>=i){
lst=min(lst,w[j].se);
j--;
}
ans+=(lst-i);
}
cout<<ans<<endl;
}
对于这种数据规模很大的,要统计个数,我们最多只能枚举一维度变量。此时我们就可以通过画图来解决,也就是固定端点做法。
1883D-In Love *1500 思维+STL
- 题意:你有 $T$ 次操作,每次操作为添加或删除一条区间范围为 $[l,r]$ 的线段,问每次操作后是否存在两条线段,使得它们的区间范围没有交集。
- 思路:对于线段的维护,我们可以用
multiset来维护,然后判断是否存在两条线段,使得它们的区间范围没有交集,我们就可以取最左边的线段和最右边的线段。在实际代码中,我们也就是开两个multiset,一个放左端点,一个放右端点。此时如果右端点最小的端点都小于我们左端点最大的点,那么肯定是存在。
int n;
multiset<int>s1,s2;
char s;
void solve() {
cin>>n;
while(n--){
int l,r;
cin>>s>>l>>r;
if(s=='+'){
s1.insert(l);
s2.insert(r);
}else{
s1.erase(s1.find(l));
s2.erase(s2.find(r));
}
if(s1.sz>1){
if(*s2.begin()<(*s1.rbegin())){
YES;
}else{
NO;
}
}else{
NO;
}
}
}
multiset比set能够处理重复元素。
1925B-A Balanced Problemset? *1200 数学
- 题意:给定 $n,x$,要求你把 $n$ 拆成 $x$ 个数,使得它们的 $\gcd$ 最大。
- 思路:对于这种拆数问题并且要求 $\gcd$ 最大,那么我们可以考虑把这个数质因数分解。也就是变成 $x=p_1{x_1}p_2...p_n^{x_n}$ 的形式,如果求 $\gcd$ 的话,就是求这些数指数的 $\min$,反之求 $\max$。那这道题的结论就是答案就是 $n$ 的因数。以下是证明过程:
证明
void solve() {
cin>>x>>n;
int ans=1;
for(int i=1;i<=x/i;i++){
if(x%i==0){
if(i>=n)ans=max(ans,x/i);
if(x/i>=n)ans=max(ans,i);
}
}
cout<<ans<<endl;
}
知道的结论:$\gcd(A,B)=\gcd(A,A+B)$(一个序列的 $\gcd$ 等于该序列前缀和后的 $\gcd$),也就是 $\gcd(a_1,a_2,a_3...)=\gcd(a_1,a_1+a_2,a_1+a_2+a_3,...)$。
1991D-Prime XOR Coloring *1900 结论题
- 题意:给定一张图。若 $u⊕v$ 为质数则 $u,v$ 间有一条连边。求最少颜色数,使得将图染色后,没有一条边的两个端点的颜色相同,并给出每个点的染色方案。
- 思路:我们发现如果两个整数之差为 $4$ 的倍数,那么他们异或也一定是 $4$ 的倍数,显然是合数。也就是说此时差值为 $4$ 的位置可以颜色相同,那意思就是说最终的答案也就只要 $4$ 种颜色。
void solve() {
cin>>n;
if(n==1){
cout<<1<<endl<<1<<endl;
}
if(n==2){
cout<<2<<endl<<1<<' '<<2<<endl;
}
if(n==3){
cout<<2<<endl<<"1 2 2"<<endl;
}
if(n==4){
cout<<"3\n1 2 2 3"<<endl;
}
if(n==5){
cout<<"3\n1 2 2 3 3"<<endl;
}
if(n==6){
cout<<"4\n1 2 2 3 3 4"<<endl;
}
if(n>6){
cout<<4<<endl;
FOR(i,1,n){
cout<<(i-1)%4+1<<" \n"[i==n];
}
}
}
很重要的结论:如果两个整数之差为 $4$ 的倍数,那么他们异或也一定是 $4$ 的倍数,显然是合数。
739A-Alyona and mex *1700 构造
- 题意:你有 $m$ 个区间,要求构造一个长度为 $n$ 的序列使得这 $m$ 个区间中 $mex$ 最小的最大($mex$ 定义为一个区间内没有出现过的最小自然数)。
- 思路:我们先把问题简单化,也就是如果只有一个区间的情况下,此时我们的 $mex$ 值就等于区间的长度。那么当我们出现多个区间的话,那么答案就是这些区间长度的最小值。那么方案怎么输出呢?我们可以让整个数组的 $mex$ 值都是那个最小值,也就是比如
1 2 0 1 2 0 1 2 ...这样的数组,这样你任意取一段区间长度大于最小值的它的 $mex$ 都是一样的。
void solve() {
cin>>n>>m;
FOR(i,1,m)cin>>l[i]>>r[i];
int ans=1e18;
FOR(i,1,m){
ans=min(ans,r[i]-l[i]+1);
}
cout<<ans<<endl;
FOR(i,1,n){
cout<<i%ans<<' ';
}
}
这道题的构造先得考虑特殊情况,再考虑一般情况。也就是先得想最一般的构造是怎样的,然后通过数学归纳法得出结论(或者可以猜结论)。
从这里开始,我都会分析我自己错在哪里了。
1690E-Price Maximization *1500 思维
-
题意:给你 $n$ 件物品,再给你 $n/2$ 个包裹,每个包裹可以放 $2$ 件物品,每个包装的成本是包的重量除 $k$ (给定值)下取整的值,求最大可能总成本。
-
正解思路:首先我们可以先算所有数对 $k$ 下取整的值,题目要求最大总成本,因为我们有可能选的两个数不会导致被下取整(就是刚好整除这样的),因此我们得维护每个包裹重量模 $k$ 的余数,然后我们对余数排序,那么此时的余数就具备单调性了,然后我们就用双指针去维护就可以了(也就是如果再 $l<r$ 的范围内出现 $a_l+a_r>=k$ 答案就得加一)
-
卡在:不知道怎么去处理包裹重量和的下取整,没有想到要维护余数来做。(
其实我第一眼看到 $k$ 很小,就打算从 $k$ 入手,没想到就不知道怎么下去了)
void solve() {
cin>>n>>k;
int ans=0;
FOR(i,1,n)cin>>w[i],a[i]=w[i]%k,ans+=w[i]/k;
sort(a+1,a+1+n);
int l=1,r=n;
while(l<r){
while(l<r&&a[l]+a[r]<k)l++;
if(l<r)ans++;
l++,r--;
}
cout<<ans<<endl;
}
1789C-Serval and Toxel's Arrays *1500 思维题
-
题意:给定一个不重的数组和 $m$ 次操作,每次操作更改数组中的一个数,时刻保证数组不重。求包括的初始数组的 $m+1$ 个数组两两的并集的元素个数之和。
-
思路:我们发现,对于第 $i$ 次修改操作,计算当前数组与前 $i$ 个历史版本的贡献。因为数组不重,所以并集中一定有自身的 $n$ 个元素,可以先给答案增加 $i \times n$。前面这句话很好理解。接下来计算前 $i$ 个历史版本出现但是当前不存在的数字的个数,下文记为 $sum$。统计进答案即可。也就是说,在修改操作时同时维护 $sum$。如将 $p$ 位置更改为 $q$,我们就可以给 $sum$ 加上 $a_p$ 之前出现的次数,减去 $q$ 之前对 $sum$ 的贡献 $del_{q}$(其中 $dek_{q}$ 指的是上次 $a_q$ 被替换完到现在被替换的操作次数,然后 $add$ 数组指的是 $q$ 最后一次出现的操作次数。总的来说就是,$del$ 维护的是被删除掉的数的贡献,为什么要维护这个呢?是因为如果后面一不小心又把数换回来,如果不减掉的话,此时这样我们是默认那个被删掉的数仍然还在数组里面的,那么两个数组并集就会重复计算了。就是为了避免重复(也就是如果这个数之前出现过后面被删了然后后面又出现然后又被删了,此时前面的
i-add[w[x]]+del[w[x]]计算就会重复))。维护这些信息,只需要记录每一个数字出现的时间和删除时给 $sum$ 的贡献即可。总的来说这种思路就是按每个数字出现次数来算贡献的,而我的思路就是看成一个整体来做,这样就有明显问题。 -
错误思路:就是我没有考虑到一个数还有可能会重新在后面出现出现,然后它可能会贡献答案。当时考虑的太理想了。具体思路就是:因为每两个变化的数组都会被计算一下,此时我就先算出除了原数组以外其他数组相比元素组数的变化情况,如果在某个位置变化则在这个地方+1。(思路到这里就彻底错了)
void solve() {
cin>>n>>m;
FOR(i,1,n)cin>>w[i];
VI del(n+m+1,0),add(n+m+1,0);
int sum=0,ans=0;
FOR(i,1,m){
int x,y;
cin>>x>>y;
int t=i+del[w[x]]-add[w[x]];
sum+=t;
del[w[x]]=t;
add[y]=i;
sum-=del[y];
ans+=n*i+sum;
w[x]=y;
}
cout<<ans<<endl;
}
特难理解我觉得
1792C-Min Max Sort *1500 思维
- 题意:给你一个数组,每次操作可以取出两个数 $x,y$,较大的数放在数组末尾,较小的数放在开头。问多少次操作能让数组升序。
- 思路:因为这道题给定是序列,因此你要升序,此时就得是按照这样
1 2 3 4...这样排列。同时我们也发现,最大的交换次数就是 $n/2$ 。什么时候会比那个少呢?那当然就是比如2 1 4 3,此时只需要一次操作,那么我们发现:我们从已排序的数组角度出发,分别位于首尾的 $1$ 和 $n$ 一定是在最后一次操作中归位的。类似地,$2$ 和 $n−1$ 一定是在倒数第二次操作中归位的。以此类推。假设共操作 $k$ 次,则 $1$∼$k$ 和 $n−k+1$∼$n$ 都被归位,而中间的 $k+1$∼$n−k$ 按原顺序排列。设元素 $i$ 的初始位置为 $pos_i$ 。那么 $k$ 次操作能将 $p$ 排序,当且仅当 $pos_{k+1} \lt pos_{k+2}\lt \cdots\lt pos_{n-k}$ 。首先 $k_0=\lfloor\frac n2\rfloor$ 一定可行。如果 $k$ 可行,并且 $pos_k\lt pos_{k+1},~pos_{n-k}\lt pos_{n-k+1}$ ,那么 $k-1$ 也可行。从 $k_0$ 开始枚举,找到最小的 $k$ 即可。注意这里的 $pos_i$ 指的是 $w_x=i$,也就是如果要变成有序的情况下,这个地方填的数是原数组索引的哪个位置。
void solve() {
cin>>n;
FOR(i,1,n){
int x;
cin>>x;
w[x]=i;
}
int ans=n/2;
while(ans&&w[ans]<w[ans+1]&&w[n-ans]<w[n-ans+1])ans--;
cout<<ans<<endl;
}
其实发现还可以用二分来做。
1817A-Almost Increasing Subsequence *1500 前缀和
- 题意:如果数组不包含满足 $z \le y \le x$ 的情况,那么说明这个序列是几乎递增的,对于每组询问数组中几乎递增的长度。
- 思路:因为只需要不存在三元组,那么如果是两个数的话,那么它肯定不存在这个情况。那么这个我们可以用前缀和来维护,只有当三元组结束了才 $+1$。询问时,只需要用区间的长度减去区间当中有多少满足条件的元素即可。但需要注意的是,$s[l]$ 由 $w[l-1]$ 和 $w[l-2]$ 决定,$s[l+1]$ 由 $w[l]$ 和 $w[l-1]$ 决定,含有不在区间内的元素,所以算的时候应该从 $s[l+2]$ 开始算,当然还要减 $1$。所以满足要求的元素个数为 $s[r]-s[(l+2)-1]$,即 $s[r]-s[l+1]+2$。
int n,m;
int w[N],s[N];
void solve() {
cin>>n>>m;
FOR(i,1,n)cin>>w[i];
FOR(i,2,n){
if(i>1&&!(w[i-2]>=w[i-1]&&w[i-1]>=w[i])){
s[i]=s[i-1]+1;
}else{
s[i]=s[i-1];
}
}
while(m--){
int l,r;
cin>>l>>r;
if(r-l<2){
cout<<r-l+1<<endl;
}else{
cout<<(s[r]-s[l+1]+2)<<endl;
}
}
}
前缀和也是可以维护三元甚至四元组的情况。
1842C-Tenzing and Balls *1500 dp
-
题意:给定数组 $a$,可选择 $i,j$,$1\le i < j\le |a|$ 且 $a_i = a_j$,删去 $[i, j]$ 的所有数,后面的数(如果有)接上,问最多能删几个数。
-
思路:这是很典型的线性 dp 问题,每个数可以选和不选,我们可以设 $f_i$ 表示前 $i$ 个数里面能删多少个数,则有:$f_i=\max(f_{j-1}+i-j+1,f_i)$,其中 $a_i=a_j$,如果我们暴力求 $j$ 就肯定超时了,因为我们根据 $a_i=a_j$ 可以知道,此时可以用桶来装,这个桶 $mx_x$ 表示对于所有的 $x$ 满足 $a_i=x$ 最大的 $f_{j-1}+i-j+1$。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
VI f(n+1,0),mx(n+1,-1e18);
FOR(i,1,n){
f[i]=max(f[i-1],mx[w[i]]+i);
mx[w[i]]=max(mx[w[i]],f[i-1]-i+1);
}
cout<<f[n]<<endl;
}
这道题真服了,我没有想到用dp来做,因为我看数据规模很大。以后得注意了,数据规模比较大也得想想dp,因为可能有些性质可以让dp变成线性dp的。因此我们得多列列状态转移方程。
1876B-Effects of Anti Pimples *1500 贡献法
- 题意:你有一个长为 $n$ 的序列 $a(1≤n≤10^5)$。现在你可以在序列中可重地选若干个下标,这个位置和下标的倍数的位置也会被选。获得的价值是选出的位置上数的最大值,不能不选。你显然有 $2^n−1$ 种选法,现在请你求出这些选法的价值总和,模 $998244353$。
- 思路:肯定是贡献法。我们可以发现,如果其中如果选择了对应位是最大值的下标,那么无论其他的,结果一定是最大值。一次往下,选择了次大值的下标,结果一定是次大值。我们可以考虑预处理每一个下标 $i$,看它以及的所有倍数下标 $j$ 所能产生的最大值是多少。预处理完之后,我们先看看,所有含最大值的子集有多少个,答案是 $2^{n-1}$ 个。以下是推理:含某一个数字的子集个数,我们可以看作是总的子集个数减去不含那个数字的子集个数。不含那个数,集合大小就是 $n−1$,子集个数为 $2^{n−1}$−1。
int n,w[N];
int qmi(int a,int b){
int res=1;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
void solve() {
cin>>n;
FOR(i,1,n){
cin>>w[i];
}
FOR(i,1,n){
for(int j=i;j<=n;j+=i){
w[i]=max(w[i],w[j]);
}
}
sort(w+1,w+1+n);
int ans=0;
int tot=n;
FOR(i,1,n){
ans=(ans+qmi(2,i-1)%mod*w[i]%mod)%mod;
}
cout<<ans%mod;
}
其实也可以不必推导,直接得出结论。
1793C-Dora and Search *1200 STL
- 题意:找到一个子段满足 $a_l\not = \min(a_l,a_{l+1},...,a_r),a_r\not = \max(a_l,a_{l+1},...,a_r)$,或者a_l\not = \max(a_l,a_{l+1},...,a_r),a_r\not = \min(a_l,a_{l+1},...,a_r)$。如果存在输出下标。
- 思路:这又是一道维护区间最大值最小值问题(而且这些数可能重复),这种问题要么用线段树/ST表/树状数组维护,但写的比较多,此时也可以用
multiset维护。然后后面就模拟即可。
void solve() {
cin>>n;
multiset<int>S;
FOR(i,1,n)cin>>w[i],S.insert(w[i]);
int l=1,r=n;
while(l<r){
int mx=*S.rbegin(),mi=*S.begin();
int a=((mx==w[l])||(mi==w[l]));
int b=((mx==w[r])||(mi==w[r]));
if(a&&b)S.erase(w[l++]),S.erase(w[r--]);
else if(a)S.erase(w[l++]);
else if(b)S.erase(w[r--]);
else{
cout<<l<<' '<<r<<endl;
return;
}
}
cout<<-1<<endl;
}
维护区间最大值最小值问题(而且这些数可能重复),这种问题要么用线段树/ST表/树状数组/multiset维护。
1822D-Super-Permutation *1200 思维
- 题意:给定排列 $a$,构造数组 $b$,其中 $b_i=(a_1+a_2+...+a_i)\mod n$,如果 $a$ 数组是超级排列,当且仅当 $[b_1+1,b_2+2,...,b_n+1]$ 是排列。
- 思路:我们可以通过暴力打表来找规律,发现奇数是不可能的(除 $1$ 外),如果是偶数的话,则可以这样构造: $n,n-1,2,n-3,4,n-5,6...$。
void solve() {
cin>>n;
if(n==1){
cout<<1<<endl;
return;
}
if(n&1){
cout<<-1<<endl;
return;
}
if(n==2){
cout<<"2 1"<<endl;
return;
}
int p1=2,p2=n-1;
cout<<n<<' ';
while(p2!=1){
cout<<p2<<' '<<p1<<' ';
p1+=2,p2-=2;
}
cout<<p2<<endl;
}
1932C-Contrast Value *1200 思维
- 题意:给你一个数组 $a$,我们称相邻两个数的绝对值为对比度,你可以删除 $a$ 数组一些元素,要求你删除的数越多(即 $a$ 数组的长度越小)且对比度不变。
- 思路:很简单,我们发现如果是连续的递减或递增,此时中间的数都可以删掉了。
void solve() {
cin>>n;
map<int,int>S;
FOR(i,1,n)cin>>w[i],S[w[i]]++;
if(S[w[1]]==n){
cout<<1<<endl;
return;
}
int ans=1;
FOR(i,1,n-1){
if(w[i]==w[i+1]){
;
}else if(w[i]<w[i+1]){
int j=i;
while(j+1<=n&&w[j+1]>=w[j])j++;
ans++;
i=j-1;
}else{
int j=i;
while(j+1<=n&&w[j+1]<=w[j])j++;
ans++;
i=j-1;
}
}
cout<<ans<<endl;
}
1870B-Friendly Arrays *1200 位运算+思维
- 题意:给定长度为 $n$ 的数组 $a$ 和长度为 $m$ 的数组 $b$,此时可以无限次进行操作 $a_i|=a_j$,问操作完成后 $a$ 数组的异或和的最大值和最小值。
- 思路:我们可以分奇偶来讨论,当 $n$ 为奇数的情况下,此时异或要最大,也就是每个数都给它搞奇数个 $1$,也就是说我们可以把 $b$ 的每个数都或起来,然后再全部给 $a$ 数组或上,这样对于每一位都有奇数个 $1$ 了,那么异或起来肯定大,此时的最小值很明显就是原异或值,反之偶数情况。
int n,m;
int a[N],b[N];
void solve() {
cin>>n>>m;
int ans1=0,ans2=0;
FOR(i,1,n){
cin>>a[i];
ans1^=a[i];
}
FOR(i,1,m){
cin>>b[i];
ans2|=b[i];
}
int ans=0;
FOR(i,1,n){
ans^=(ans2|a[i]);
}
if(n&1){
cout<<ans1<<' '<<ans<<endl;
}else{
cout<<ans<<' '<<ans1<<endl;
}
}
异或这玩意得考虑奇数偶数情况。
1877C-Joyboard *1200 数学
- 题意:给你三个整数 $n,m,k$,你将会选一个数 $x(0 \le x \le m)$ 作为 $a_{n+1}$ 的值,然后从 $n$ 到 $1$ 遍历使得 $a_i=a_{i+1} \bmod i$。问有多少个这样的数 $x$,使得 $a_i$ 的值恰好有 $k$ 种。
- 思路:这就是一道诈骗题。我们可以发现我们只需要讨论 $k<=3$ 的情况就好,因为不可能有超过 $3$ 的情况。当 $k=1$ 时候,此时 $a_{n+1}=0$ ,否则 $a$ 序列一定会取到至少两个值。故输出 $1$。当 $k=2$ 时候,此时这要求值一步从初始值变为 $0$,显然初始值要么小于 $n$ 要么是 $n$ 的倍数。故输出 $\min(n−1,m)+⌊n/m⌋$。当 $k=3$:总数减去 $k<3$ 的情况,故输出 $m−\min(n−1,m)−⌊n/m⌋$。
void solve() {
cin>>n>>m>>k;
if(k==1){
cout<<1<<endl;
}else if(k==2){
cout<<min(n-1,m)+m/n<<endl;
}else if(k==3){
cout<<m-min(n-1,m)-m/n<<endl;
}else{
cout<<0<<endl;
}
}
1903E-StORage room *1200 位运算+思维
- 题意:给出一个矩阵 $m$,问是否能构造一个序列 $a$,满足 $a_{i}|a_{j}=m_{i,j}(i \ne j)$,如果有,输出这个矩阵。
- 思路:我们可以发现:给我们的矩阵如果不考虑对角线上的,如果某个数的某一位有 $1$ 的情况下,那么最后的或值就是有 $1$。那么这里可以用一个技巧,一般的,对于已知或值求某个数的位数状态,我们可以用与来做,具体来说就是先把每个数的位数都变成 $1$,然后如果不存在 $1$ 的话,此时这个数就是 $0$。
void solve() {
cin>>n;
FOR(i,1,n){
FOR(j,1,n){
cin>>w[i][j];
}
}
VI f(n+1,(1<<30)-1);
FOR(i,1,n){
FOR(j,1,n){
if(i!=j){
f[i]&=w[i][j];
f[j]&=w[j][i];
}
}
}
FOR(i,1,n){
FOR(j,1,n){
if(((f[i]|f[j])!=w[i][j])&&i!=j){
NO;
return;
}
}
}
YES;
FOR(i,1,n){
cout<<f[i]<<" \n"[i==n];
}
}
这种与和或互相转化的思路得好好了解。
1919C-Grouping Increases *1400 思维题
-
题意:给定一个长为 $n$ 的序列 $a$,你需要将该序列恰好分成两个子序列 $s,t$。定义一个长为 $m$ 的序列 $b$ 的代价为 $ p(b)=\sum_{i=1}^{m-1}[b_i<b_{i+1}]$,求 $p(s)+p(t)$ 的最小值。每个测试点 $t$ 组测试用例。
-
思路:为了避免我们下面的错误思路,我们可以把原数组当成一个堆,然后从堆中取出一些数放在两个数组中去,这里我们把这两个数组的末尾给维护下。此时我们就会考虑:对于一个新元素 $a_i$ ,我们要考虑将其放入哪个子序列后,而且我们希望修改后 $x,y$ 的值尽量变大,其中 $x,y$ 指的是两个序列的末尾值,因为这会对后续造成更有利的影响。(这种放入的贪心比我们在原数组的贪心就少了后效性)然后我们只需要分类讨论即可。也即是:当 $a_i>x$ 这意味着无论放在哪个子序列都会导致答案多一,但是显然将 $a_i$ 放在 $y$ 最优,因为这会使 $x,y$ 增大的尽量多(如果你放在 $x$ 后面,那么你 $y$ 就不大了),$a_i>y$ 我们就把数放在 $y$ 后面,$a_i \le y$ 显然此时放在哪个子序列中答案都不会加一。但是这样修改后一定会让它们变小,所以我们就放在 $y$ 这里了。
-
错误思路:认为当出现递增的数就把数放在第二个数组中去。那么这个很明显就是错的,因为:
1 2 3 9 6 3 1输出了 $3$,但答案是 $2$。
void solve() {
cin>>n;
FOR(i,1,n)cin>>w[i];
int ans=0;
int x=1e18,y=1e18;
//之前做法错了原因:1 2 3 9 6 3 1 输出了3,但答案是2
//因为我们这样子有可能会因为数特别大,然后第一位可以牺牲一次法式换来后面的舒服。
FOR(i,1,n){
if(w[i]<=x){
x=w[i];
}else if(w[i]>x){
if(w[i]<=y){
y=w[i];
}else{
x=w[i];
swap(x,y);
ans++;
}
}
}
cout<<ans<<endl;
}
1974D-Ingenuity-2 *1400 模拟
- 题意:给定一个长度为 $n$ 且仅包含
N、S、E、W的指令串,将这些指令分给一个的机器人和直升机,它们会沿着这个方向移动一步,问它们能否从同一个点走到另一个相同的点? - 思路:我们其实可以在如果
N是奇数的情况下给R,如果是偶数的情况下给H,这样一定是最优的,因为不管怎样二者都有移动过。然后最后只需要判断是不是会重合即可。
void solve() {
cin>>n>>s;
int c1=0,c2=0,c3=0,c4=0;
string ans(n,' ');
int i=0;
for(auto x:s){
if(x=='N'){
c1++;
if(c1&1)ans[i]='R';
else ans[i]='H';
}
if(x=='S'){
c2++;
if(c2&1)ans[i]='R';
else ans[i]='H';
}
if(x=='W'){
c3++;
if(c3&1)ans[i]='H';
else ans[i]='R';
}
if(x=='E'){
c4++;
if(c4&1)ans[i]='H';
else ans[i]='R';
}
i++;
}
int rx=0,ry=0,hx=0,hy=0;
bool f1=false,f2=false;
int idx=0;
for(auto x:ans){
if(x=='R')f1=true;
if(x=='H')f2=true;
if(x=='R'&&s[idx]=='N')ry++;
if(x=='R'&&s[idx]=='S')ry--;
if(x=='R'&&s[idx]=='W')rx--;
if(x=='R'&&s[idx]=='E')rx++;
if(x=='H'&&s[idx]=='N')hy++;
if(x=='H'&&s[idx]=='S')hy--;
if(x=='H'&&s[idx]=='W')hx--;
if(x=='H'&&s[idx]=='E')hx++;
idx++;
}
if(hx==rx&&hy==ry&&f1&&f2)cout<<ans<<endl;
else{
NO;
}
}
别想太复杂了,这是我之前的代码:(写的跟屎山一样)
void solve() {
cin>>n>>s;
if(s=="SN"||s=="NS"||s=="EW"||s=="WE"){
NO;
return;
}
map<char,int>S;
for(auto x:s){
S[x]++;
}
if((S['N']+S['S'])%2||(S['E']+S['W'])%2){
NO;
return;
}
int cn=0,ce=0,cw=0,cs=0;
bool f=false;
if((S['N']&1)||(S['S']&1)){
int t=max(S['S']-1,S['N']-1);
if(!t){
cs++;
cn++;
f=true;
}else{
if(S['S']>S['N']){
cs+=t;
cn+=S['N'];
}else if(S['S']<S['N']){
cn+=t;
cs+=S['S'];
}else{
cs+=S['S'];
}
}
}else if((S['N'])||(S['S'])){
int p1=S['N']/2,p2=S['S']/2;
if(p1>1){
cn+=p1;
}else if(p1){
cn++;
}
if(p2>1){
cs+=p2;
}else if(p2){
cs++;
}
}
if((S['E']&1)||(S['W']&1)){
int t=max(S['E']-1,S['W']-1);
if(!t){
if(!f){
ce++;
cw++;
}
}else{
if(S['E']>S['W']){
ce+=t;
cw+=S['W'];
}else if(S['E']<S['W']){
cw+=t;
ce+=S['E'];
}else{
ce+=S['E'];
}
}
}else if(S['E']||S['W']){
int p1=S['E']/2,p2=S['W']/2;
if(p1>1){
ce+=p1;
}else if(p1){
ce++;
}
if(p2>1){
cw+=p2;
}else if(p2){
cw++;
}
}
// cout<<cn<<' '<<cw<<' '<<cs<<' '<<ce<<endl;
string ans;
for(auto x:s){
if(x=='S'&&cs)cs--,ans+="R";
else if(x=='S'&&!cs)ans+="H";
if(x=='W'&&cw)cw--,ans+="R";
else if(x=='W'&&!cw)ans+="H";
if(x=='E'&&ce)ce--,ans+="R";
else if(x=='E'&&!ce)ans+="H";
if(x=='N'&&cn)cn--,ans+="R";
else if(x=='N'&&!cn)ans+="H";
}
cout<<ans<<endl;
}

浙公网安备 33010602011771号