good problems-8
1.E - Red and Blue Graph
题意:n个点m条边的图,选k个点染色,使得特殊边的个数为偶数,特殊边指连接这条边的2点一个染色一个未染色
题解:
由此可见,特殊边的奇偶性和选的k个点的度数和的奇偶性相同
那么我们进行枚举,枚举奇数度数的点选x个,偶数度数的点选k-x个
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
ll power(ll x,ll y){
ll ans=1;
while(y){
if(y&1)ans=ans*x%MOD;
y>>=1;x=x*x%MOD;
}
return ans;
}
ll fac[maxn],inv[maxn];
ll C(ll n,ll m){
if(m>n)return 0;
if(m==0 || n==m)return 1ll;
ll ans=fac[n]*inv[m]%MOD;
ans=ans*inv[n-m]%MOD;
return ans;
}
ll n,m,k,in[maxn];
int main(){
n=read();m=read();k=read();
fac[0]=1ll;
for(ll i=1;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
inv[n]=power(fac[n],MOD-2);
for(ll i=n-1;i;i--)inv[i]=inv[i+1ll]*(i+1ll)%MOD;
for(int i=1;i<=m;i++){
int x=read(),y=read();
in[x]++;in[y]++;
}
ll cnt1=0,cnt2=0;
//cnt1奇数度数个数
for(int i=1;i<=n;i++){
if(in[i]&1)cnt1++;
else cnt2++;
}
ll ans=0;
for(ll i=0;i<=min(k,cnt1);i+=2){
ans=ans+C(cnt2,k-i)*C(cnt1,i)%MOD;
//可能k-i大于cnt2
ans%=MOD;
}
cout<<(ans%MOD+MOD)%MOD;
return 0;
}
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
void solve(){
int n=read(),m=read();
vector<ll>sum(n);
for(int i=0;i<n;i++){
ll pre=0;
for(int j=1;j<=m;j++)pre+=read(),sum[i]+=pre;
}
ll mx=*max_element(sum.begin(),sum.end());
ll mi=*min_element(sum.begin(),sum.end());
int pos=min_element(sum.begin(),sum.end())-sum.begin()+1;
printf("%d %lld\n",pos,mx-mi);
return ;
}
int main(){
int t=read();
while(t--)solve();
return 0;
}
3.E. Count Seconds
用拓扑排序求dp
因为根据结论1,当一个点由点权大于0变成0后一直都是0
但存在图中并没有流到的点,也就是存在0--》大于0--》0
我们dp时只能根据 大于0--》0来进行dp,因为我们没法计算0--》大于0的时间
其实dp式子成立要保证一流一出
可以画一个链来模拟试试
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
ll n,m,a[maxn],in[maxn];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){
to[++tot]=y;nx[tot]=head[x];head[x]=tot;
}
void solve(){
n=read();m=read();
int cnt=0;
for(int i=1;i<=n;i++){
a[i]=read();in[i]=head[i]=tot=0;
if(a[i])cnt++;
}
for(int i=1;i<=m;i++){
int x=read(),y=read();
in[y]++;add(x,y);
}
if(!cnt){puts("0");return ;}
for(int k=1;k<=n;k++){
vector<int>b(n+1);
for(int i=1;i<=n;i++)b[i]=a[i];
for(int u=1;u<=n;u++){
if(a[u]){
b[u]--;
for(int i=head[u];i;i=nx[i]){
b[to[i]]++;
}
}
}
cnt=0;
for(int i=1;i<=n;i++){
a[i]=b[i];
if(b[i])cnt++;
}
if(!cnt){cout<<k<<endl;return ;}
}
queue<int>q;
vector<ll>dp(n+1);
for(int i=1;i<=n;i++){
if(in[i]==0)q.push(i);
}
int last=0;
while(q.size()){
int u=q.front();q.pop();
dp[u]+=a[u];dp[u]%=MOD;
last=u;
for(int i=head[u];i;i=nx[i]){
int v=to[i];
dp[v]+=dp[u];
dp[v]%=MOD;
in[v]--;
if(in[v]==0)q.push(v);
}
}
cout<<(dp[last]+n)%MOD<<endl;
return ;
}
int main(){
int t=read();
while(t--)solve();
return 0;
}
4.[SCOI2009]粉刷匠
首先每个木板是相互独立的,不难首先想到设\(dp_{i,j}\)表示前i个木板用了j次操作的最大值
接下来就是如何处理每个木板,也就是如何求出转移方程
如果我们知道\(f_{i,j}(第i块木板用j次操作的最大值)\),那么状态很好表示
\(dp_{i,j}=max_{0\leq k \leq j}(dp_{i-1,k}+f_{i,j-k})\)
考虑如何求得f数组
当前的二维数组无法表示清楚第i块木板,不妨增加一维
设\(f_{i,j,k}\)表示第i个木板的前j个格子用了k次操作的最大值
\(f_{i,j,k}=max_{0\leq t <j}(f_{i,t,k-1}+w_{i,t+1,j})\)
\(w_{i,l,r}\)表示第i个木板,粉刷一次在l到r的格子上的最大值
那么就能得到f数组
那么
\(dp_{i,j}=max_{0\leq k \leq j}(dp_{i-1,k}+f_{i,m,j-k})\)
总结:
实际是一个分组背包问题——将每条木板看成一个物品组,则物品组中每个物品\(i\)的费用\(c_i\)和价值\(w_i\)分别为操作次数\(k\)和\(f_{i,m,k}\)。所求为从每组中最多选一件,求解将哪些物品装入背包可使这些物品的费 用总和不超过背包容量T,且价值(粉刷正确的格子数)总和最大。
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,m,t;
int dp[51][2501],f[51][51][2501],w[51][51];
int main(){
n=read();m=read();t=read();
for(int i=1;i<=n;i++){
string a;cin>>a;
for(int j=1;j<=m;j++){
int cnt0=0,cnt1=0;
for(int k=j;k<=m;k++){
if(a[k-1]=='0')cnt0++;
else cnt1++;
w[j][k]=max(cnt0,cnt1);
}
}
for(int j=1;j<=m;j++)for(int k=1;k<=m;k++){
for(int t=0;t<j;t++){
f[i][j][k]=max(f[i][j][k],f[i][t][k-1]+w[t+1][j]);
}
}
}
for(int i=1;i<=n;i++)for(int j=0;j<=t;j++){
for(int t=0;t<=j;t++){
dp[i][j]=max(dp[i][j],dp[i-1][t]+f[i][m][j-t]);
}
}
cout<<dp[n][t]<<endl;
return 0;
}
5.F - Tournament
题意:有\(2^n\)个人,最开始1和2比赛,3和4比赛····,随后1和2号的赢者与3和4号的赢者比赛,
以此类推进行n轮比赛,最后只剩一人。\(c_{i,j}\)表示i号赢了j场比赛的奖金,求比赛结束后,每个人的奖金之和最大。
题解:
可以把比赛过程图当成一颗满二叉树
设\(dp_{i,j}\)表示i连续赢了j场比赛,i所在子树的最大值(不包含当前i节点的奖金)
假设求\(dp_{1,2}\),转移来自另一个非1所在的子树的结果
\(dp_{1,2}=dp_{1,1}+max(dp_{3,1}+c[3][1],dp_{4,1}+c[4][1])\)
所以
\(dp_{i,j}=dp_{i,j-1}+max_v (dp_{v,j-1}+c[v][j-1])\)
一个一个枚举肯定不行,可以预处理出最大值来转移
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
ll n,c[maxn][18],dp[maxn][18];
ll getmaxx(int l,int r,int dep){
ll maxx=0;
for(int i=l;i<=r;i++)maxx=max(maxx,dp[i][dep]+c[i][dep]);
return maxx;
}
void solve(int k,int l,int r,int dep){
if(l==r)return;
int mid=(l+r)>>1;
solve(k<<1,l,mid,dep-1);solve(k<<1|1,mid+1,r,dep-1);
ll maxl=getmaxx(l,mid,dep-1),maxr=getmaxx(mid+1,r,dep-1);
for(int i=l;i<=mid;i++)dp[i][dep]=dp[i][dep-1]+maxr;
for(int i=mid+1;i<=r;i++)dp[i][dep]=dp[i][dep-1]+maxl;
return ;
}
int main(){
n=read();
for(int i=1;i<=(1<<n);i++)for(int j=1;j<=n;j++)c[i][j]=read();
solve(1,1,(1<<n),n);
ll ans=0;
for(int i=1;i<=(1<<n);i++)ans=max(ans,dp[i][n]+c[i][n]);
printf("%lld\n",ans);
return 0;
}
6.E. Swap and Maximum Block
题意:
有一个长度为\(2^n\)的数组a。接着有q个操作,每个操作给定一个k。交换\(a_i,a_{i+2^k}\)所有对之后(从前往后交换,如果某个位置已经和前面交换,则跳过),输出得到的数组的最大子段和。
每次询问后,改变当前的数组。
题解:
这道题有几个性质需要发现:
- 考虑交换的性质,不难发现对于k来说,把数组每\(2^{k+1}\)分成一段,这一段的前\(2^k\)平移到后\(2^k\),后\(2^k\)平移到前\(2^k\)
- 交换偶数次相同的k,数组不发生改变
- 交换是可交换的。例如先k=1后k=2和先k=2和后k=1是一样的
由性质2和3,我们可以把当前操作表示成2进制数,从低到高第i位表示操作k=i是否交换
由性质1,我们把数组区间按照二叉树分段,像线段树那样是一棵满二叉树。操作k,则相当于对树中倒数第k+1层的所有节点的两个儿子进行交换(从0开始)。
同时,线段树也可以很好求出最大子段和。不妨线段树和状态表示进行结合,我们把每个状态的答案求出,最后询问\(O(1)\)给出结果。
我们设\(tr[t][st]\)表示线段树上t节点所表示区间的操作状态为st时子段的信息(包含前缀最大值,后缀最大值,区间总和,当前区间状态位st的答案)
对于t节点所表示区间可以翻转和不翻转两种可能进行枚举
st'表示子区间的状态,k表示t节点交换左右儿子的操作位置
不翻转:\(tr[t][st']=tr[t<<1][st']+tr[t<<1|1][st']\)
翻转 :\(tr[t][st'|1<<k]=tr[t<<1][st']+tr[t<<1|1][st']\)
这里为什么左右儿子状态st'是一样的?因为操作是对整体数组操作的,左儿子进行的操作,右儿子一定进行了
这里的重载+
struct node{
ll lmx,rmx,sum,ans;
node(ll x,ll y,ll z,ll t){
lmx=x;rmx=y;sum=z;ans=t;
}
node operator +(node x){
node y=node(0,0,0,0);
y.lmx=max(lmx,sum+x.lmx);
y.rmx=max(x.rmx,x.sum+rmx);
y.sum=sum+x.sum;
y.ans=max(rmx+x.lmx,max(ans,x.ans));
return y;
}
};
这样按着线段树递归求解就行
但是st的状态为\(2^n\)数组开不下且超时
但是子区间的状态就减半了,枚举也减半了
比如倒数第二层有\(2^{n-1}\)个点,每个点有\(2\)种可能,到数第二层的状态为\(2^n\)
倒数第三层有\(2^{n-2}\)个点,每个点有\(2^2\)种可能,到数第二层的状态为\(2^n\)
以此类推,有n层,那么时间和空间复杂度为\(O(n*2^n)\)
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e5+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
ll n,q,a[maxn];
struct node{
ll lmx,rmx,sum,ans;
node(ll x,ll y,ll z,ll t){
lmx=x;rmx=y;sum=z;ans=t;
}
node operator +(node x){
node y=node(0,0,0,0);
y.lmx=max(lmx,sum+x.lmx);
y.rmx=max(x.rmx,x.sum+rmx);
y.sum=sum+x.sum;
y.ans=max(rmx+x.lmx,max(ans,x.ans));
return y;
}
};
vector<node>tr[maxn<<3];
void build(int k,int l,int r){
if(l==r){
tr[k].pb(node(max(0ll,a[l]),max(0ll,a[l]),a[l],max(0ll,a[l])));
return ;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);build(k<<1|1,mid+1,r);
int lr=(k<<1),rc=(k<<1|1);
//当前位为0
for(int i=0;i<tr[lr].size();i++){
tr[k].pb(tr[lr][i]+tr[rc][i]);
}
//当前位为1
for(int i=0;i<tr[lr].size();i++){
tr[k].pb(tr[rc][i]+tr[lr][i]);
}
return ;
}
int main(){
n=read();
for(int i=1;i<=(1<<n);i++)a[i]=read();
q=read();int now=0;
build(1,1,(1<<n));
while(q--){
int x=read();
now^=(1<<x);
printf("%lld\n",tr[1][now].ans);
}
return 0;
}
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,a[1001][1001],f[maxn];
int find(int x){
if(f[x]==x)return x;
return f[x]=find(f[x]);
}
int same(int x,int y){
return find(f[x])==find(f[y]);
}
void merge(int x,int y){
int xx=find(x),yy=find(y);
f[xx]=yy;
return ;
}
int vis[maxn];
void solve(){
n=read();
for(int i=1;i<=2*n;i++)f[i]=i,vis[i]=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)a[i][j]=read();
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(a[i][j]>a[j][i]){
if(same(i,j))continue;//如果之前的点连过边了,就不再连了,因为之前的点字典序小
//i-->j+n,i+n-->j
//我们假设j+n和j是换的,通过这样连边,可以使得i不换,i+n不换
//相当于小于等于n的点连向大与n的点(也就是让大于n当祖先),这样这个联通块中小于等于n的点的祖先都是大于n的,他们就是不选
//如果大于n的点连向小于等于n的点,与上面情况相反
merge(i,j+n);
merge(i+n,j);
//j>i
//所以要不然最后find(j)==j
//就是存在k>j,使得find(j)==k+n,但是一定有find(k)==k
}
else if(a[i][j]<a[j][i]){
if(same(i,j+n))continue;
merge(i,j);
merge(i+n,j+n);
}
}
}
map<int,int>mm;
for(int i=1;i<=n;i++){
if(find(i)==i)mm[i]=1;
//肯定会有find(i)==i
//因为肯定会存在一些集合的祖先是i,那么这个集合中小于等于n的点都是换的
}
for(int k=1;k<=n;k++){
if(mm[find(k)]==0)continue;
//交换k行k列
for(int i=1;i<=n;i++)swap(a[i][k],a[k][i]);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)printf("%d ",a[i][j]);puts("");
}
return ;
}
int main(){
int t=read();
while(t--)solve();
return 0;
}
8.Z-Game on grid
题意:Alice先手,Bob后手,问Alice是否有必赢、必平、必输的三种可能。
题解:
有一个性质是关键,Alice只会走到\((i+j)%2==0\)的点上,Bob与之相反
那么我们从后往前推,假设推Alice必赢的可能
假如一个点\((i,j)\),是Alice在的位置,他可以走到\((i+1,j)和(i,j+1)\)两个Bob可能的所在点
那么他肯定会走其中一个能够必胜的点(或者2个点都不能必胜,走哪个都行)
相反,那么Bob肯定会走2个点必输的点
那么我们设\(dp_{i,j}=0/1\),0表示走到\((i,j)\)必输,1表示必赢
1.\((i+j)%2==1, dp_{i,j}=min(dp_{i+1,j},dp_{i,j+1})\)
2.\((i+j)%2==0,dp_{i,j}=max(dp_{i+1,j},dp_{i,j+1})\)
如果当前点是’A’,那么\(dp_{i,j}\)直接为1,如果是‘B’,\(dp_{i,j}\)直接为0,‘.’情况不变(按照上述转移方程转移)
求必平,必输可能与上述类似
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,m,bk[501][501],dp[501][501];
int get_dp(int now){
memset(dp,0,sizeof(dp));
if(bk[n][m]==now)dp[n][m]=1;
else dp[n][m]=0;
for(int j=m;j>=1;j--){
for(int i=n;i>=1;i--){
if(i==n && j==m)continue;
if(i==n)dp[i][j]=dp[i][j+1];
else if(j==m)dp[i][j]=dp[i+1][j];
else if((i+j)&1)dp[i][j]=min(dp[i+1][j],dp[i][j+1]);
else dp[i][j]=max(dp[i+1][j],dp[i][j+1]);
//平局
if(now==0 && bk[i][j]!=now)dp[i][j]=0;
//A&B
if(bk[i][j]==now && now>0)dp[i][j]=1;
else if(now && bk[i][j]!=now && bk[i][j]!=0)dp[i][j]=0;
}
}
return dp[1][1];
}
void solve(){
n=read();m=read();
for(int i=1;i<=n;i++){
string a;cin>>a;
for(int j=1;j<=m;j++){
bk[i][j]=0;
if(a[j-1]=='A')bk[i][j]=1;
else if(a[j-1]=='B')bk[i][j]=2;
}
}
if(get_dp(1))printf("yes ");
else printf("no ");
if(get_dp(0))printf("yes ");
else printf("no ");
if(get_dp(2))puts("yes");
else puts("no");
return;
}
int main(){
int t=read();
while(t--)solve();
return 0;
}
9.D. Empty Graph
题意:给定一长度为n的数组,你可以进行k次操作任意改变数组的一个值,用该数组构建一张无向稠密图,对于任意[l, r]两点之间的路径长度为数组a上[l, r]范围内的最小值。求操作后该图上两个点之间最短路的最大值。
题解:
可以先从性质出发:最短路只有两种取得的方式,一种是从u直接走到v(也就是u和v相邻)。还有一种是u先走到权值最小的点,然后再走到v去,那么路径长度是数组上最小权值的两倍。
如何操作能构造出最长的最短路呢?考虑贪心
\(ans=min( minn*2 , MAX( min(a[i],a[i+1]) ) )\)
2种情况分类讨论
- 将k小数变为1e9,使得minn最大
- 将k-1小数变为1e9,使得MAX( min(a[i],a[i+1]) )最大
为什么要分为2种情况,因为第一种情况不一定是最优的,第一种情况我们期望ans=2*minn,但是ans可能等于MAX( min(a[i],a[i+1]) )
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=3e6+101;
const int MOD=1e9+7;
const int inf=1e9;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct wzq{
int val,id;
}a[maxn],b[maxn];
void solve(){
int n=read(),k=read();
for(int i=1;i<=n;i++)a[i].val=read(),a[i].id=i,b[i]=a[i];
int ans=0;
//ans=min(minn*2, MAX( min(a[i],a[i+1]) ) )
//将k小数变为1e9,使得minn最大
{
sort(a+1,a+n+1,[](wzq i,wzq j){
return i.val<j.val;
});
for(int i=1;i<=k;i++)a[i].val=inf;
int minn=a[k+1].val;
if(k+1>n)minn=inf;
sort(a+1,a+n+1,[](wzq i,wzq j){
return i.id<j.id;
});
for(int i=1;i<n;i++){
ans=max(ans,min(a[i].val,a[i+1].val));
}
ans=min(ans,2*minn);
//期望ans=2*minn,但是ans可能等于MAX( min(a[i],a[i+1]) )
}
//将k-1小数变为1e9,使得MAX( min(a[i],a[i+1]) )最大
{
sort(b+1,b+n+1,[](wzq i,wzq j){
return i.val<j.val;
});
if(k-1>0)ans=max(ans,min(b[k].val*2,inf));
else ans=max(ans,min(b[n].val,b[k].val*2));
//注意k=1的时候单独考虑
}
cout<<ans<<endl;
return ;
}
int main(){
int t=read();
while(t--)solve();
return 0;
}
10.E2. LCM Sum (hard version)
题意:t组数据,每次给定\([l,r]\),求出当前范围内满足条件的三元组\((i,j,k),i<j<k\)的个数。条件是:\(lcm(i,j,k)\geq i+j+k\)
这道题分为easy和hard版本,easy版本 \(1\leq t\leq 5\) , hard版本 \(t\leq 10^5\),所有版本\(1\leq l \leq r\leq 2*10^5\)
题解:
对于这种有限制条件的计数题,一定要尝试反着考虑;或者当满足条件的个数很接近所有可能的个数的时候考虑反着想
反着的情况就是统计\(lcm(i,j,k) < i+j+k <3k\)的个数:
那么就两种可能
- \(lcm(i,j,k)=k, \:\:i+j+k<k(一定满足)\)
- \(lcm(i,j,k)=2k, i+j+k<2k \:\: =>i+j<k(不一定满足要考虑)\)
那么easy的做法就是
\([l,r]\)枚举k
- 对于第一种情况我们就枚举k的因子,然后求出\((i,j)\)的个数 \((i<j \:\: 且\:\: i,j\in k的因子)\)
但是第一种情况不需要枚举,设k的因子为cnt,那么\((i,j)\)个数=\(C_{cnt}^2\) - 对于第一种情况我们就枚举2k的因子,然后求出第一种情况没计算的\((i,j)\)的个数 \((i<j \:\: 且 \:\: i+j< k \:\: 且\:\: i,j\in 2k的因子)\),这种情况得枚举\((i,j)\)
虽然因子个数总数接近\(nlogn\),但是不是每个数的因子个数是\(logn\)的
这就导致第二种情况枚举(i,j)的总时间复杂度接近\(O(t*n\sqrt n)\)会超时的
但是通过打表发现,第二种情况很少,只有形如\((3t,4t,6t)\:\:\: (6t,10t,15t) , t\in Z\) 这样的数,才满足\(lcm(i,j,k) = 2*k\)
那么我们单独累计就行
那么easy做法就如下
点击查看代码
vector<vector<int> >fac(400000+1);
void solve() {
int L=read(),R=read();
ll len=R-L+1;
ll ans=len*(len-1)*(len-2)/6;
for (int k=L;k<=R;k++) {
ll cnt=0;
for(auto x:fac[k])if(L<=x && x<k)cnt++;
ans-=cnt*(cnt-1)/2;
if(k%6==0 && k/2>=L)ans--;
if(k%15==0 && k*2/5>=L)ans--;
}
printf("%lld\n",ans);
return ;
}
int main(){
//nlogn 求约数
for (int i = 1;i<=2e5;i++) {
for (int j=i;j<= 2e5;j+=i)fac[j].push_back(i);
}
int t=read();
while(t--)solve();
return 0;
}
对于hard来说,\(t=10^5\)
我们离线做,因为\(i,j,k\in[l,r],i<j<k\)
设\(f[i]\)为\((i,j,k)\)的个数
假设当前k=8,约数有1,2,4,8
那么增加的贡献就是f[1]+=2,f[2]+=1,f[4]+=0,f[8]+=0
区间\([l,r]\)的答案就是就是\(f[l]+····+f[r-2]\)
单点修改,区间查询,用树状数组维护即可
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n=maxn;
struct Tree{
ll f[maxn];
int lowbit(int x){return x&(-x);}
void add(int pos,ll val){
for(int i=pos;i<=n;i+=lowbit(i))f[i]+=val;
}
ll query(int pos){
ll ans=0;
for(int i=pos;i;i-=lowbit(i))ans+=f[i];
return ans;
}
ll range_sum(int l,int r){
return query(r)-query(l-1);
}
}T;
struct info{
ll l,r,ans;
int id;
}q[maxn];
vector<vector<int> >fac(2e5+1);
int main(){
//nlogn 求约数
for (int i = 1;i<=2e5;i++) {
for (int j=i;j<=2e5;j+=i)fac[j].push_back(i);
}
int t=read();
for(int i=1;i<=t;i++){
q[i].l=read(),q[i].r=read();q[i].id=i;
ll len=q[i].r-q[i].l+1;
q[i].ans=len*(len-1)*(len-2)/6ll;
/*
去掉3 4 6和6 10 15的情况
统计2*L <= 6倍数 <=R 个数=R/6-2*(L-1)/6=R/6-(L-1)/3
统计5*L/2 <=15倍数 <=R 个数=R/15-5*(L-1)/2/15=R/15-(L-1)/6
*/
q[i].ans-=max(0ll,q[i].r/6-(q[i].l-1)/3);
q[i].ans-=max(0ll,q[i].r/15-(q[i].l-1)/6);
}
sort(q+1,q+t+1,[](info i,info j){
return i.r<j.r;
});
int l=0;
for(int i=1;i<=t;i++){
while(l<q[i].r){
l++;
int sz=fac[l].size()-2,len=sz;
for(int j=0;j<len;j++){
T.add(fac[l][j],sz);
sz--;
}
}
q[i].ans-=T.range_sum(q[i].l,q[i].r);
}
sort(q+1,q+t+1,[](info i,info j){
return i.id<j.id;
});
for(int i=1;i<=t;i++)printf("%lld\n",q[i].ans);
return 0;
}