2019 Multi-University Training Contest 2(1005|1008|1009|1010|1011|1012)

1005.(hdu6595)Everything Is Generated In Equal(概率dp)

给你一个N(N<=3e3),从1-N中随机选出一个数n,不妨令ans[i]为长度为i的排列的逆序对期望

先计入ans[n],再将该排列变成n的一个子序列(不妨长度为p,0<=p<=n),

再统计其期望值ans[p],再重复该过程,直到子序列长度为0为止

求对于给定的n,统计进去的期望值的和是多少

 

思路来源:https://www.cnblogs.com/nlKOG/p/11243080.html

 

概率dp,首先如果不考虑向子序列转移,

n的排列中共有C_{n}^{2}=\frac{n*(n-1)}{2}个逆序对,在一半的排列中他们表现为逆序对,另一半表现为正序对,所以出现概率\frac{1}{2}

故计ans[i]为长度为i的排列的逆序对期望,ans[i]应为\frac{C_{n}^{2}}{2}

剩下的,概率dp的套路了,dp[n]=dp[n]+所有dp[子情况]

以上图片来自思路来源,由于右边有f(n)所以需移项,具体处理时注意对f(n)分子分母同乘2^{n},处理好逆元

最后res[i]先求一次前缀和,再对每项乘一下N的逆元即可

当然,如果你能找出规律,直接输出\frac{n^{2}-1}{9}

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=998244353; 
const int maxn=3e3;
ll Finv[maxn+5],jc[maxn+5];
ll n,ans[maxn+5];
ll modpow(ll x,ll n,ll mod)
{
	ll res=1;
	for(;n;x=x*x%mod,n/=2)
	if(n&1)res=res*x%mod;
	return res;
}
ll inv(ll x)
{
	return modpow(x,mod-2,mod);
}
void init()
{
	jc[0]=Finv[0]=1;
	for(int i=1;i<=maxn;++i)
	{
	 jc[i]=jc[i-1]*i;
	 if(jc[i]>=mod)jc[i]%=mod;
    }
	Finv[maxn]=modpow(jc[maxn],mod-2,mod);
	for(int i=maxn-1;i>=1;--i)
	{
	 Finv[i]=Finv[i+1]*(i+1);
	 if(Finv[i]>=mod)Finv[i]%=mod;
    }
}
ll C(ll n,ll m)
{
	if(m<0||m>n)return 0;
	return jc[n]*Finv[n-m]%mod*Finv[m]%mod;
}
void init2()
{
	ll v=inv(4);
	for(ll i=2;i<=maxn;++i)
	{
		ans[i]=i*(i-1)*v%mod;
		ans[i]=ans[i]*modpow(2,i,mod)%mod;
		for(ll j=0;j<i;++j)
		ans[i]=(ans[i]+C(i,j)*ans[j])%mod; 
		ans[i]=ans[i]*inv(modpow(2,i,mod)+mod-1)%mod;
	}
	for(ll i=2;i<=maxn;++i)
	ans[i]=(ans[i]+ans[i-1])%mod;
	for(ll i=2;i<=maxn;++i)
	ans[i]=ans[i]*inv(i)%mod;
}
int main()
{
	init();init2();
	while(~scanf("%lld",&n))
	printf("%lld\n",ans[n]);
	return 0;
} 

1008.(hdu6598)Harmonious Army

有一个由n(n≤500)个人组成的军队,每个人都可以担任战士或法师。

军队中存在m(m≤1e4)个关系,第i个关系中包括两个人li,ri。

当两人均为战士时,军队总战斗力上升Ai,

当一人为战士一人为法师时,军队总战斗力上升Bi,

当两人均为法师时,军队总战斗力上升Ci,

保证Bi=Ai/4+Ci/3且Bi为整数,求军队能获得的最大总战斗力。

 

思路来源:https://blog.csdn.net/weixin_42789801/article/details/97287137

 

ç½ç»æµå»ºå¾

最大流构建不出模型来,拆成两个点却只能流其中一个点,所以考虑最小割模型

先假设获得了所有收益Ai+Bi+Ci,再使损失最小

x和y同属s集合时,代表两人同为战士,获得Ai,也就是割掉边c和边d,损失Bi+Ci;

x和y同属t集合时,代表两人同为法师,获得Ci,也就是割掉边a和边b,损失Ai+Bi;

其他情况,比如割ad,或者割bc,获得Bi,损失的是Ai+Ci

 

联立方程组\begin{bmatrix} a+b=A+B \\c+d=B+C \\ a+d+e=A+C \\ b+c+e=A+C \end{bmatrix}

注意只有e=\frac{A+C}{2}-B是唯一解,剩下的赋a=b,c=d即可

解得一组解\begin{bmatrix} a=b=(A+B)/2 \\c=d=(B+C)/2 \\ e=(A+C-2B)/2 \\ \end{bmatrix},边权扩大二倍跑最小割,最后流量除以二即可

最后ans等于-mincut()+\sum(Ai+Bi+Ci)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<map> 
using namespace std;
typedef long long ll;
const ll INF=0x3f3f3f3f3f3f3f3fll;
const int maxn=510;
const int maxm=5e4+10;//2个点10条边 1e4个点 
int level[maxn];
int head[maxn],cnt;
int n,m;
int ss,ee;
struct edge{int v,nex;ll w;}e[maxm*2];
void init()
{
	cnt=0;
	memset(head,-1,sizeof head);
}
void add(int u,int v,ll w)
{
	e[cnt].v=v;
	e[cnt].w=w;
	e[cnt].nex=head[u];
	head[u]=cnt++;
}
bool bfs(int s,int t)
{
	queue<int>q;
	memset(level,0,sizeof level);
	level[s]=1;
	q.push(s);
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		if(x==t)return 1;
		for(int u=head[x];~u;u=e[u].nex)
		{
			int v=e[u].v;ll w=e[u].w;
			if(!level[v]&&w)
			{
				level[v]=level[x]+1;
				q.push(v);
			}
		}
	}
	return 0;
}
ll dfs(int u,ll maxf,int t)
{
	if(u==t)return maxf;
	ll ret=0;
	for(int i=head[u];~i;i=e[i].nex)
	{
		int v=e[i].v;ll w=e[i].w;
		if(level[u]+1==level[v]&&w)
		{
			ll MIN=min(maxf-ret,w);
			w=dfs(v,MIN,t);
			e[i].w-=w;
			e[i^1].w+=w;
			ret+=w;
			if(ret==maxf)break;
		}
	}
	if(!ret)level[u]=-1;//优化,防止重搜,说明u这一路不可能有流量了 
	return ret;
}
ll Dinic(int s,int t)
{
	ll ans=0;
	while(bfs(s,t))
	ans+=dfs(s,INF,t);
	return ans;
}
int main()
{ 
    //n个点 m条边 
    while(~scanf("%d%d",&n,&m))
    {
    	init();
		ss=n+1;
		ee=n+2;
		ll ans=0;
    	for(int j=0;j<m;++j) 
    	{
    		int u,v;
			ll a,b,c;
			//有向图反向边权为0 无向图反向边权为w
			//边权放大为原来的2倍 
    		scanf("%d%d%lld%lld%lld",&u,&v,&a,&b,&c);
    		ans+=(a+b+c)*2;
    		add(ss,u,a+b);add(u,ss,0);
    		add(ss,v,a+b);add(v,ss,0);
    		add(u,ee,b+c);add(ee,u,0);
    		add(v,ee,b+c);add(ee,v,0);
    		add(u,v,a+c-2*b);add(v,u,a+c-2*b);
    	}
    	ans-=Dinic(ss,ee);
    	printf("%lld\n",ans/2);
    }
	return 0;
}

1009.(hdu6599)I Love Palindrome String(回文树+manacher)

要统计所有[l,r]为回文串且[l,(l+r)/2向下取整]为回文串,

每出现这样一个[l,r],其贡献算在i=(r-l+1)的长度i里,对i的贡献+1

对于每个长度i,输出其贡献

 

思路来源:福州大学标程代码

 

用回文树先处理出所有本质不同的子串,每一个子串对应一个节点

在节点i里记录该回文串长度为len[i],其最后一个字母在PAM中s的位置为no[i]

则该回文串其实际上对应在串中的位置为[no[i]-len[i]+1,no[i]],

manacher预处理原序列,注意PAM中下标为1-n,而manacher中是0-(n-1)

处理好之后,直接判前半段和后半段是否至少有一段为回文串即可(其实似乎判一段就行了)

 

看懂了代码,但是不会自己敲,只好加点注释之后,粘标程

人家的manacher非常短,调下标还不麻烦,以后就用这个了,省去调下标的过程

人家的回文树的板子也是功能很全,还支持fail树上倍增的操作

#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define per(i, a, b) for(int i=(b)-1; i>=(a); i--)
#define sz(a) (int)a.size()
#define de(a) cout << #a << " = " << a << endl
#define dd(a) cout << #a << " = " << a << " "
#define all(a) a.begin(), a.end()
#define pw(x) (1ll<<(x))
#define endl "\n"
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
typedef double db;
#define mem(a,x) memset(a,x,sizeof(a))

const int N=3e5+5;
char s[N];
int pa[N<<1],n,ret[N];

void Manacher(char *s,int n,int *pa){
	//s[0]对应pa[0]
	//s[1]对应pa[2]
	//s[i]对应pa[2*i]
	//奇数位置为原#号位置 此处pa半径是不考虑#的 但考虑中心 如aba对于b的回文半径为2 
    pa[0] = 1;
    for(int i=1,j=0;i<(n<<1)-1;++i){
        int p = i >> 1 , q = i - p , r = ((j + 1)>>1) + pa[j] - 1;
        pa[i] = r < q ? 0 : min(r - q + 1 , pa[(j<<1) - i]);
        while(0 <= p - pa[i] && q + pa[i] < n && s[p - pa[i]] == s[q + pa[i]])
            pa[i]++;
        if(q + pa[i] - 1 > r) j = i;
    } 
}

inline bool check(int L,int R) {//询问[L,R]是否为回文串 0<=L<=R<n
    int mid=L+R>>1;
    //实质是 回文半径 应为 串长/2向上取整 aba对应2 bb对应1 
    if ((L&1)==(R&1)) return pa[mid<<1]>=R-L+2>>1;//长度为奇的串 a#b#a 0-2 (R-L+2)/2==2 即ab的长度 
    return pa[mid<<1|1]>=R-L+1>>1;//长度为偶的串 b#b 0-1 (R-L+1)/2==1 即b的长度 (不考虑#)
}

inline void Put(string &s,vi &V) {
    V.clear();
    rep(i,0,sz(s)) V.pb(s[i]-'a');
}

// 关于((L&1)!=(R&1))的说明 
// LR同奇偶 串长为奇 后半段应从mid开始 故不加 
// LR异奇偶 串长为偶 后半段应从mid+1开始 故+1 
inline bool Check(int L,int R) {
    int mid=L+R>>1;
    if (!check(L,mid) || !check(mid+((L&1)!=(R&1)),R)) return false;
    return true;
}

const int M=26;
struct PAM{
    int s[N],len[N],next[N][M],fail[N],cnt[N],dep[N],id[N],no[N],last,n,p,cur,now,f[N][20]; 
	ll ans;
    inline int new_node(int _l) { mem(next[p],0); cnt[p]=dep[p]=0,len[p]=_l; return p++; }
    inline void Init() { new_node(p=0),new_node(s[0]=-1),fail[last=n=0]=1; }
    inline int get_fail(int x) { for (; s[n-len[x]-1]!=s[n]; x=fail[x]); return x; }
    inline void I(int c) { 
        c-='a',s[++n]=c,cur=get_fail(last);
        if (!next[cur][c]) {
            now=new_node(len[cur]+2);
            fail[now]=next[get_fail(fail[cur])][c];
            next[cur][c]=now;
            dep[now]=dep[fail[now]]+1; 
        }
        last=next[cur][c]; cnt[last]++; 
		id[n]=last,no[last]=n; //id[第n个字符]=第i个节点 no[第last个节点]=第n个字符 记录下在原串中的位置下标 从1开始 
    }
    inline void Insert(char s[],int op=0,int _n=0) {//正倒序插入 
        if (!_n) _n=strlen(s);  if (!op) rep(i,0,_n) I(s[i]); else per(i,0,_n) I(s[i]); 
    }
    inline void count() { per(i,0,p) cnt[fail[i]]+=cnt[i]; }
    inline int Find(int x,int L) {//去找节点x的回文串长不超过l的最近祖先(可以为自己)编号 倍增lca 
        if (len[x]<=L) return x;
        per(i,0,20) if (len[f[x][i]]>L) x=f[x][i];
        return fail[x];
    }
    inline void init() {//fail树预处理倍增 
        mem(f,0);
        rep(i,0,p) f[i][0]=fail[i];
        rep(i,0,p) rep(j,1,20) f[i][j]=f[f[i][j-1]][j-1];
    }
    inline void Q() {
        count();
        rep(i,2,p) no[i]--;//为了对应0-(n-1)的下标 
        rep(i,0,p) if (len[i]>0)
            ret[len[i]]+=Check(no[i]-len[i]+1,no[i])*cnt[i];
			//每个本质不同的回文串 用整段去判前半段是否回文 
    }
} TT;

int main() {
    
    while (scanf("%s",s)!=EOF) {
        n=strlen(s); Manacher(s,n,pa);
        mem(ret,0);
        TT.Init(),TT.Insert(s),TT.Q();
        rep(i,1,n+1) printf("%d%c",ret[i]," \n"[i==n]);
    }
    
    return 0;
}

1010.(hdu6600)Just Skip The Problem

水题,输出n(n<=1e9)的阶乘mod p(p==1e6+3)

 

1011.(hdu6601)Keen On Everything But Triangle(主席树or划分树)

N(N<=1e5)个火柴棒,第i根长度为ai(1<=ai<=1e9)

Q(Q<=1e5)个询问,第j次询问只用[lj,rj]内的火柴棒,

询问若其中取出三根构成三角形,三角形的最大周长是多少

 

划分树不会,用的主席树

如果不能构成火柴棒,增长最密集的数列是斐波那契数列,即第三个数等于前两个数之和

但即便是fib数列,增长到1e9也只需要44项,这说明1e9内密集的数,44个数中一定会出现三根火柴棒

于是变成询问区间第一大到区间44大问题,主席树板子题,

板子是个从小到大的第kth,那就倒着询问,从第len开始呀,

 

注意少mod,本来想保留123向234转移的时候只把1丢掉,沿用23的结果,

后来发现这不如直接重问快,因为写一个循环的三个节点每次都得mod

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=1e5+10;
//主席树 与上一棵树共用没有新开的那些节点 
//root数组代表第i颗树的根 
//插入和查询都是 两棵树同步走相对应的节点 
typedef long long ll;
int cnt,root[maxn],n,m,l,r;
int a[maxn],Rank[maxn];
ll res,ans[3];
struct node
{
	int lson,rson,num;
	//分别是左子下标 右子下标 这段值域出现的个数 
}tree[maxn*40];
//pre代表上一颗子树的根 cur代表当前子树的根 v代表要插的值 
void add(int pre,int &cur,int l,int r,int v) 
{
    //cur的引用保证了cur赋新值之后,能直接更新递归上层的tree[cur].son
	cur=++cnt;
	tree[cur].num=tree[pre].num+1;
	if(l==r)return;
	tree[cur].lson=tree[pre].lson,tree[cur].rson=tree[pre].rson;
	int mid=(l+r)>>1;
	if(v<=mid)add(tree[pre].lson,tree[cur].lson,l,mid,v);
	else add(tree[pre].rson,tree[cur].rson,mid+1,r,v);
}

int query(int L,int R,int l,int r,int kth)
{
	if(l==r)return l;//二分区间直至单点满足 不满足返回0 
	int mid=(l+r)>>1;
	int diff=tree[tree[R].lson].num-tree[tree[L].lson].num;
	if(diff>=kth)return query(tree[L].lson,tree[R].lson,l,mid,kth);
	else return query(tree[L].rson,tree[R].rson,mid+1,r,kth-diff);
}
int main()
{
	//确保所有值都在1到n之间 如果不在的话 离散化一下就搞成1到n之间了
	//插第一条链的时候 不用管pre节点里值是什么 认为全都是0也无妨 
	//也可以先build一棵普通权值线段树 然后再add节点 
	root[0]=0,tree[0].lson=tree[0].rson=tree[0].num=0;
	while(~scanf("%d%d",&n,&m))
	{
	cnt=0;
	for(int i=1;i<=n;++i)
	{
		int v;
		scanf("%d",&a[i]);
		Rank[i]=a[i];
	}
	sort(Rank+1,Rank+n+1);//离散化一波 
	int mx=unique(Rank+1,Rank+n+1)-(Rank+1);
	//printf("%d\n",mx);
	for(int i=1;i<=n;++i)
	{
		a[i]=lower_bound(Rank+1,Rank+mx+1,a[i])-Rank;//保证最小也是1 现在ai只是一个排名 
		add(root[i-1],root[i],1,mx,a[i]);
	}
	//前缀和思想 [l,r]信息只需将第root[r]棵树与第root[l-1]棵树作差 
	while(m--)
	{
		scanf("%d%d",&l,&r);
		int len=r-l+1;
		if(len<3){puts("-1");continue;}
		res=-1;
		for(int rk=len;rk>=3;--rk)
		{
			for(int j=0;j<3;++j)
			ans[j]=Rank[query(root[l-1],root[r],1,mx,rk-j)];
			if(ans[0]<ans[1]+ans[2])
			{
				res=ans[0]+ans[1]+ans[2];
				break;
			}
		}
		printf("%lld\n",res);//要返回原值啊 
    }
	}
	return 0;
}

1012.(hdu6602)Longest Subarray(线段树+双端队列)

n(n<=1e5)个数,每个数在1到C之间(C<=1e5),设最少出现次数为K(K<=1e5)

求一个最长的子区间[l,r],使得[l,r]内的元素,均出现不少于K次

 

特判k==1的情形,为长度n

其余情况,考虑线段树+双端队列,

last[v]记v上一次出现的位置,加上一个值v的时候,先将v单点赋值成C,

再将[last[v]+1,v]区间-1,代表这一段因为一个值次数小于K从而不可取,并加入双端队列

当双端队列长度为k时,将k处的区间的值+1,代表如果固定右端点为现在的v的时候,

左端端点是可以取到恢复了的位置,即单点值等于C的位置的,

那么端点如果有一个值小于C,说明至少受一个值的约束从而不可取,

所以对于枚举的每个右端点,答案就是满足单点值加回C的最左端点,更新答案即可

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<deque>
using namespace std;
const int maxn=1e5+10;
int dat[maxn*5],cov[maxn*5];
int n,c,k,ans,a[maxn];
deque<int>e[maxn];
void build(int p,int l,int r)
{
	dat[p]=cov[p]=0;
	if(l==r)return;
	int mid=(l+r)/2;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r); 
}
void pushup(int p)
{
	dat[p]=max(dat[p<<1],dat[p<<1|1]);
	//其实是维护这两段和是否大于0  
	//正+正>0,负+负<0 如果两段和>0一定是大正+小负 
}
void pushdown(int p,int l,int r)
{
	if(cov[p])
	{
		int mid=(l+r)/2;
		dat[p<<1]+=cov[p];
		dat[p<<1|1]+=cov[p]; 
		cov[p<<1]+=cov[p];
		cov[p<<1|1]+=cov[p];
		cov[p]=0;
	}
}
void update(int p,int l,int r,int ql,int qr,int v)
{
	if(ql<=l&&r<=qr)
	{
		dat[p]+=v;
		cov[p]+=v;
		return;
	}
	pushdown(p,l,r);
	int mid=(l+r)/2;
	if(ql<=mid)update(p<<1,l,mid,ql,qr,v);
	if(qr>mid)update(p<<1|1,mid+1,r,ql,qr,v); 
	pushup(p);
}
int ask(int p,int l,int r)
{
	if(l==r)return dat[p]==c?l:-1;
	pushdown(p,l,r);
	int mid=(l+r)/2;
	if(dat[p<<1]==c)return ask(p<<1,l,mid);
	if(dat[p<<1|1]==c)return ask(p<<1|1,mid+1,r);//不会询问到[i+1的区域 因为没更新 
	return -1;
}
int main()
{
	while(~scanf("%d%d%d",&n,&c,&k))
	{
		ans=0;
		for(int i=1;i<=n;++i)
		scanf("%d",&a[i]);
		if(k==1){printf("%d\n",n);continue;}
		for(int i=1;i<=c;++i)
		{
			e[i].clear();
			e[i].push_back(0);
		}
		build(1,1,n);
		for(int i=1;i<=n;++i)//枚举右端点i 满足条件的应为左端被拔回原高的 
		{
			int v=a[i];
			update(1,1,n,i,i,c);//初始高度为c  
			if(e[v].size()==k)
			{
				int l=e[v].front();
				e[v].pop_front();
				int r=e[v].front();
				update(1,1,n,l+1,r,1);//满足连续k个的条件 将这一段+1升回来 
			}
			int x=e[v].back();
			update(1,1,n,x+1,i,-1);//新加的这一段不符合条件 降下去-1 
			e[v].push_back(i);
			int pos=ask(1,1,n);//查询最左 高度为c的值 
			if(pos!=-1)ans=max(ans,i-pos+1);
		}
		printf("%d\n",ans);
	}
	return 0;
} 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值