状压dp总结

状压dp总结


模型

枚举子集

1. O ( 3 n ) O(3^n) O(3n)

对于每一位,有3种状态:在原集和子集中,在原集中不在子集中,在子集中不在原集中。

for(int i=S;i>=1;i=(i-1)&S)
2. O ( 2 n ) O(2^n) O(2n)

考虑建立两层之间的转移,注意是从大的转移到小的,还是相反。

这里是用大的更新小的。

st=(1<<n)-1;
for(ll i=0;i<n;++i)//!!
	for(ll j=st;j>=0;--j)//!!
		if(!(j&(1<<i)))	num[j]+=num[j|(1<<i)];

注意事项:

1.当使用滚动数组优化时,满足情况就转移,不要忘记不满足的情况(直接赋值)。

2.求S对U的补集用C=S^U

51nod 1779逆序对统计
题意:有n道题,每道题有一些分数值(一共m个分数值)可以取到。每个分数值对应一道题。求每道题分数值形成的序列中,逆序对的个数最多是多少。n,m(n<=20,m<=100)

滚动数组会超时,压成一维就过了。

ll n,m,ans;
ll a[M],dp[2][(1<<20)+2];
ll Cal(ll x,ll y){
	ll ret=0;
	for(ll i=y+1;i<=20;++i){if(x&(1<<i)) ret++;}
	return ret;
}
int main(){
	scanf("%lld%lld",&n,&m);	
	ll t=(1<<n)-1;
	memset(dp,-inf,sizeof(dp));
	dp[0][0]=0;dp[1][0]=0;
	for(ll i=1;i<=m;++i){
		scanf("%lld",&a[i]);a[i]-=1;
		int nw=i&1,ps=(i-1)&1;
		for(ll st=0;st<=t;++st){
			if(st&(1<<a[i]))	
				dp[nw][st]=max(dp[ps][st],dp[nw][st^(1<<a[i])]+Cal(st,a[i]));
			else dp[nw][st]=dp[ps][st];//!!!!!!
		}
	}
	printf("%lld\n",dp[m&1][t]);
	return 0;
}

2.

对于 if((s&(1<< i))>0)

(s&(1<< i))外面的括号一定要打。

对于if((i&(1<< j))>0)一定不能判成1.

套路:

一.基础状压

09.18

电影:小石头喜欢看电影,选择有N部电影可供选择,每一部电影会在一天的不同时段播放。他希望连续看L分钟的电影。因为电影院是他家开的,所以他可以在一部电影播放过程中任何时间进入或退出,当然他不希望重复看一部电影,所以每部电影他最多看一次,也不能在看一部电影的时候,换到另一个正在播放一样电影的放映厅。

请你帮助小石头让他重0到L连续不断的看电影,如果可以的话,计算出最少看几部电影。

考虑定义dp[st][i]表示在i时刻看了st状态的电影。但是i太大,即使离散化也无法转移。所以我们考虑定义dp[st]表示看了状态为st的电影所需的最大时间(因为看的电影要尽量少,所以每部电影看的时间要尽量长)。如果最长时间大于L,那么统计当前看了多少部电影。

ll Cal(ll T,ll k){
	ll l = 1,r = c[k],mid,ret = -1;
	while(l <= r){
		mid = (l + r) >> 1;
		if(g[k][mid] <= T) {ret = g[k][mid] + d[k];l = mid + 1;}
		else r = mid - 1;
	} 
	if(ret <= T) return -1;
	else return ret;
}
ll Count(ll t){
	ll ret = 0;
	for(ll i = D; i >= 0; --i){
		if(t & (1 << i)) ret++;
	}
	return ret;
}
int main(){
//	freopen("c.txt","r",stdin);
	
	n = read();L = read();
	for(ll i = 0; i < n; ++i){
		d[i] = read();c[i] = read();
		for(ll j = 1; j <= c[i]; ++j){
			g[i][j] = read();
		}
	}
	ll t = (1 << n) - 1;
	for(ll st = 0; st <= t; ++st){
		for(ll i = 0; i < n; ++i){
			if((st & (1 << i)) == 0){
				dp[st | (1 << i)] = max(dp[st | (1 << i)],Cal(dp[st],i)); 
			}
		}
		if(dp[st] >= L) {
			ans = min(ans,Count(st));
//			cout << dp[st] << endl;
//			cout << Count(st) << endl;
		}
	}
	if(ans == inf) printf("-1\n");
	else printf("%lld\n",ans);
	return 0;
}

Uva 11825 - Hackers’ Crackdown
题目大意:假设你是一个黑客,侵入了了一个有着n台计算机(编号为0,1,…,n-1)的网络。一共有n种服务,每台计算机都运行着所有的服务。对于每台计算机,你都可以选择一项服务,终止这台计算机和所有与它相邻计算机的该项服务(如果其中一些服务已经停止,则这些服务继续处于停止状态)。你的目标是让尽量多的服务完全瘫痪(即:没有任何计算机运行该项服务)

输入格式:输入包含多组数据。每组数据的第一行为整数n(1<=n<=16);以下n行每行描述一台计算机的相邻计算机,其中第一个数m为相邻计算机个数,接下来的m个整数位这些计算机的编号。输入结束标志为n=0。

输出格式:对于每组数据,输出完全瘫痪的服务器的最大数量。

定义dp[st]表示入侵了状态为st的计算机。

nb[i]表示入侵第i台计算机就可以入侵的计算机。

为了转移方便,预处理出cover[i]表示当前入侵状态为i时,可以入侵的计算机。

或者说没有cover[i]就不好转移

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int N=20;

int n,num,a,testcase,ter;
int nb[(1<<17)+5],cover[(1<<17)+5],dp[(1<<17)+5];

int main(){
//	freopen("a.txt","r",stdin);
	while(~scanf("%d",&n)&&n){
		
		memset(nb,0,sizeof(nb));
		memset(cover,0,sizeof(cover));
		memset(dp,0,sizeof(dp));
		
		for(int i=0;i<n;++i){
			scanf("%d",&num);
			nb[i]|=(1<<i);
			for(int j=1;j<=num;++j){
				scanf("%d",&a);
				nb[i]|=(1<<a);
			}
		}
		ter=(1<<n)-1;
		for(int i=1;i<=ter;++i){
			for(int j=0;j<n;++j){
				if((i&(1<<j))>0){//!!!一定不能判成==1.
					cover[i]|=nb[j];
				}
			}
		}
		for(int i=1;i<=ter;++i)
			for(int j=i;j>=1;j=(j-1)&i)
				if(cover[j]==ter){
					dp[i]=max(dp[i],dp[i^j]+1);
				}
		printf("Case %d: %d\n",++testcase,dp[ter]);
	}
	return 0;
}

UVA 11795 - Mega Man’s Mission
题意:给定n代表n个敌人,第一是自己的武器,以下n行是n个敌人有的武器,消灭敌人后可以得到她的武器,武器的表示方式是一个二进制数能消灭敌人是1,不能是0,问总共有几种顺序可以消灭所有敌人。

同样状态不好转移,使用辅助状态。

定义cover[i]表示当前已经杀死状态为i的人,下一次可以杀的人的状态。

dp[i]表示杀死状态为i的敌人的方案数。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;

const ll N=20;

ll T,n,ter,testcase;
char s[N];
ll em[N],cover[(1<<17)+5],dp[(1<<17)+5];

int main(){
//	freopen("a.txt","r",stdin);
	scanf("%lld",&T);
	while(T--){
		memset(em,0,sizeof(em));
		memset(cover,0,sizeof(cover));
		memset(dp,0,sizeof(dp));
		
		scanf("%lld",&n);
		scanf("%s",s);
		ll tmp=0;
		for(ll i=0;i<n;++i) 
			if(s[i]=='1') tmp|=(1<<i);
		
		for(ll i=0;i<n;++i){
			scanf("%s",s);
			for(ll j=0;j<n;++j)
				if(s[j]=='1') em[i]|=(1<<j);
		}
		
		ter=(1<<n)-1;cover[0]=tmp;
		for(ll i=1;i<=ter;++i){
			cover[i]=tmp;
			for(ll j=0;j<n;++j){
				if((i&(1<<j)))
				cover[i]|=em[j];
			}
		}
		
		dp[0]=1;
		for(ll i=0;i<=ter;++i){
			for(ll j=0;j<n;++j){
				if((i&(1<<j))>0&&(cover[i^(1<<j)]&(1<<j))>0)
				dp[i]+=dp[i^(1<<j)];
			}
		}
		printf("Case %lld: %lld\n",++testcase,dp[ter]);
	}
	return 0;
}

二.相邻问题

考虑用当前行号、当前行及前几行的状态(对后续计算存在影响的状态)共同构成状态
1.

「BZOJ1087」[SCOI2005] 互不侵犯King
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。求方案数。

我开始想像八皇后那样dfs暴力这道题,后来发现是放k个国王,每行每列也可以放多个。是完全不同的模型。

所以先考虑如何暴力dfs:

八皇后的状态:dfs(x)表示当前在第x行,也表示放了(x-1)个棋子。因为每行只存一个,所以不需要纵坐标。

此题状态:dfs(x,y,cnt)表示当前位置在(x,y)放了cnt个棋子。

对于每个节点,可以在这一行往后走,也可以开始在下一行走。

bool Check(int x,int y){
	for(int i=0;i<=7;++i)
		if(vis[x+dx[i]][y+dy[i]]==1) return false;
	return true;
}
void dfs(int x,int y,int cnt){
	if(x>n||y>n) return;
	if(cnt==k) {
		++ans;return;
	}
	//在这一行的后面走。
	for(int i=y+1;i<=n;++i){
		if(Check(x,i)){
			vis[x][i]=1;
			dfs(x,i,cnt+1);
			vis[x][i]=0;	
		}
	}
	//在下一行走:分为走下一行的第一个和不走下一行的第一个。
	if(Check(x+1,1)){
		vis[x+1][1]=1;
		dfs(x+1,1,cnt+1);
		vis[x+1][1]=0;
	}
	dfs(x+1,1,cnt);
}
int main(){
	scanf("%d%d",&n,&k);
	//同理:第一行分为走第一个和不走第一个。
	dfs(1,1,0);
	vis[1][1]=1;
	dfs(1,1,1);
	vis[1][1]=0;
	printf("%d\n",ans);
	return 0;
}

对于正解状压dp,定义dp[i][st]表示当前在第i行,这行的状态为st的方案数。

直接枚举上一行状态会使时间爆炸,所以预处理相邻两行的状态。

我还预处理了当前行的可行状态。

交上去WA到飞起。变量重名了。

ll n,m,ans;
ll a[N],b[N],g[N][N],dp[10][N][100];

ll Cal(ll x){
	ll ret=0;
	while(x){
		if(x&1) ret++;
		x>>=1;
	}
	return ret;
}

int main(){
	scanf("%lld%lld",&n,&m);
	
	ll t=(1<<n)-1;
	for(ll i=0;i<=t;++i){
		if((i&(i<<1))==0) {
			a[i]=1;
			b[i]=Cal(i);
			//cout<<i<<' '<<b[i]<<endl;
		}
	}
	
	for(ll i=0;i<=t;++i){
		if(a[i]==1)
		for(ll j=0;j<=t;++j){
			if(a[j]==1){
				if((i&j)||(i&(j<<1))||((i<<1)&j)) continue;
				g[i][j]=1;
			}
		}
	}
	
	for(ll i=0;i<=t;++i){
		if(a[i]) {
			dp[1][i][b[i]]=1;
		//	if(b[i]==m) ans++;	
		}
	}

	for(ll i=2;i<=n;++i)
		for(ll j=0;j<=t;++j){
			if(!a[j]) continue;
			for(ll k=0;k<=t;++k){
				if(!g[k][j]) continue;
				for(ll num=b[j];num<=m;++num)
					dp[i][j][num]+=dp[i-1][k][num-b[j]];
			}
		}
	for(int i=0;i<=t;i++)ans+=dp[n][i][m];
	printf("%lld\n",ans);
	return 0;
}

炮兵阵地 POJ - 1185

这题是记录了前两行的状态。

int got(int a){int ret = 0;while(a){if(a & 1) ++ret;a >>= 1;}return ret;}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; ++i) {
		scanf("%s",s);	
		for(int j = 0; j < m; ++j) 
			if(s[j] == 'H') 
				st[i] |= (1 << j);
	}
	for(int i = 0; i < (1 << m); ++i) 
		if(!(i & (i << 1)) && !(i & (i << 2))) {
			mp[++cnt] = i;
			num[cnt] = 1;
			num[cnt] = got(i);
	}
	
	//初始化
	for(int i = 1; i <= cnt; ++i) 	
		for(int j = 1; j <= cnt; ++j)
			if(!(mp[i] & mp[j]) && !(mp[i] & st[1]))
				dp[1][i][j] = num[i];
				
	for(int now = 2; now <= n; ++now) 
		for(int i = 1; i <= cnt; ++i)
			for(int j = 1; j <= cnt; ++j)
				for(int k = 1; k <= cnt; ++k)
					if(!(mp[i] & mp[j]) && !(mp[i] & st[now]) && !(mp[k] & mp[j]) && !(mp[j] & st[now - 1]) && !(mp[k] & st[now - 2]) && !(mp[i] & mp[k]))
						dp[now][i][j] = max(dp[now][i][j],dp[now - 1][j][k] + num[i]);
	//统计答案				
	for(int i = 1; i <= cnt; ++i)
		for(int j = 1; j <= cnt; ++j)
			if(!(mp[i] & mp[j]) && !(mp[i] & st[n]) && !(mp[j] & st[n - 1]))
				ans = max(ans,dp[n][i][j]);
	printf("%d\n",ans);	
	return 0;
}

集合选数 HYSBZ - 2734
《集合论与图论》这门课程有一道作业题,要求同学们求出{1, 2, 3, 4, 5}的所有满足以 下条件的子集:若 x 在该子集中,则 2x 和 3x 不能在该子集中。同学们不喜欢这种具有枚举性 质的题目,于是把它变成了以下问题:对于任意一个正整数 n≤100000,如何求出{1, 2,…, n} 的满足上述约束条件的子集的个数(只需输出对 1,000,000,001 取模的结果),现在这个问题就 交给你了。

我们可以构造形如以下的一个矩阵

x 3x 9x 27x…

2x 6x 18x 54x

4x 12x 36x 108x

8x 24x 72x 216x

就是这种形式

那我们先令x=1吧,构造之:

1 3 9 27…

2 6 18 54…

4 12 36 108…

8 24 72 216…

我们可以观察到,每个数和他相邻的数都不可同时取,可以计算出本矩阵中取数的方案数。

但是好像又漏了一些,比如在构造的第一个矩阵中,5和25,35都没有计算到。

这时我们又要构造如下一个矩阵

5 15 45 135…

10 30 90 270…

20 60 180 540…

计算出所有矩阵的结果,因为不同矩阵间的数是一定可以共同存在的,此时乘法原理,将各矩阵求得的方案数相乘取模即为答案。

然后我们用状压dp来计算方案数。用dp[i][j]表示当前在第i行,状态为j时的方案数。

那么题目中要求的方案就是不取相邻两个数的方案数。

dp[i][j]+=dp[i-1][s]

其中j,s是合法状态,且j,s互容。

const ll mod=1000000001;
const ll n=18;
const ll m=11;
void DP(ll k){
	a[1][1]=k;
	for(ll i=2;i<=n;++i){
		a[i][1]=2*a[i-1][1];
		if(a[i][1]>num) a[i][1]=num+1;
	}
	for(ll i=1;i<=n;++i)
		for(ll j=2;j<=m;++j){
			a[i][j]=a[i][j-1]*3;
			if(a[i][j]>num) a[i][j]=num+1;
		}
	memset(b,0,sizeof(b));
	for(ll i=1;i<=n;++i)
		for(ll j=1;j<=m;++j)
			if(a[i][j]<=num){
				b[i]+=(1<<(j-1));
				vis[a[i][j]]=1;
			}
			
	memset(dp,0,sizeof(dp));
	for(int i=0;i<=b[1];++i)
		if((i&(i<<1))==0) dp[1][i]=1;
		
	for(int i=2;i<=n;++i)
		for(int j=0;j<=b[i];++j)
			if((j&(j>>1))==0)
				for(int t=0;t<=b[i-1];++t)
					if((t&(t>>1))==0&&(j&t)==0)
						dp[i][j]=(dp[i][j]+dp[i-1][t])%mod;
	ll tmp=0;
	for(int i=0;i<=b[i];++i)
		tmp=(tmp+dp[n][i])%mod;
	ans=ans*tmp%mod;
}
int main(){
//	freopen("a.txt","r",stdin);
	scanf("%lld",&num);
	for(ll i=1;i<=num;++i)
		if(!vis[i])
			DP(i);
	printf("%lld\n",ans);
	return 0;
}

三.NPC状压

当数据范围极小时,记录NPC问题的状态,从而判断当前是否满足题目条件(只走一次、全部走完……)

整张图的状压一般结合folyd或者生成树算法1.

Hie with the Pie POJ - 3311
题意:有n个点,每两个点间都存在距离,问你经过所有点,最后回到起点(0)的最短距离。

对于这种跑图的题,不可能真的跑整张图,因为转移顺序无法确定。

所以这种题往往去folyd结合,用folyd预处理出任意两点的最短路,保证点的转移不受转移顺序的影响

状压st记录每个城市是否经过。

int main(){
	while(~scanf("%d",&n)){
		if(!n) break;
		int t = (1 << (n + 1)) - 1;
		memset(dp,inf,sizeof(dp)); ans = inf; memset(w,0,sizeof(w));
		for(int i = 0; i <= n; ++i) for(int j = 0; j <= n; ++j)	scanf("%d",&w[i][j]);
		for(int k = 0; k <= n; ++k)	
			for(int i = 0; i <= n; ++i)
				for(int j = 0; j <= n; ++j)
					w[i][j] = min(w[i][j],w[i][k] + w[k][j]);	
		dp[0][1] = 0;
		for(int j = 1; j <= t; ++j)
			for(int i = 0; i <= n; ++i)
				if(j & (1 << i))
				for(int k = 0; k <= n; ++k)
					if(!(j & (1 << k)))
					dp[k][j ^ (1 << k)] = min(dp[i][j] + w[i][k],dp[k][j ^ (1 << k)]);
		for(int i = 1; i <= n; ++i) ans = min(dp[i][t] + w[i][0],ans);
		printf("%d\n",ans);
	}
	return 0;
}

POJ 2288 Islands and Bridges
题意:给出n个点,m条边。每个点有一个权值w。找出一条汉密尔顿路径,使它的值最大。一条汉密尔顿路径的值由三部分组成:

  1. 路径上每个点的权值之和
  2. 路径上每条边u-v,将其权值的积累加起来。即w[u]*w[v]
  3. 如果三个点形成一个三角形,例如i、i+1、i+2,那么将w[i]*w[i+1]*w[i+2]累加起来

汉密尔顿(哈密顿)路径,指从一个点出发,遍历所有点且只经过一次,并回到起点的路径。

d p [ s t ] [ i ] [ j ] dp[st][i][j] dp[st][i][j]表示当前状态、当前节点i、上一个节点j、边转移边统计方案数num。

int main()
{
//	freopen("test.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		cnt = 0;ans = -1;sum = 0;
		memset(w,0,sizeof(w));memset(dp,-inf,sizeof(dp));memset(num,0,sizeof(num));
		scanf("%d%d",&n,&m);
		for(int i = 0; i < n; ++i) {scanf("%d",&tt[i]);cnt += tt[i];}
		for(int i = 1; i <= m; ++i) {
			int a,b;
			scanf("%d%d",&a,&b);
			--a;--b;
			w[a][b] = w[b][a] = 1;
		}
		if(n == 1) {printf("%d 1\n",cnt);continue;}
		//初始化
		int t = (1 << n) - 1;
		for(int i = 0; i < n; ++i) 
			for(int j = 0; j < n; ++j)
				if(w[i][j] && i != j) {
				dp[(1 << i) | (1 << j)][i][j] = tt[i] * tt[j];
				num[(1 << i) | (1 << j)][i][j] = 1;
			}
				
		for(int i = 1; i <= t; ++i)
			for(int j = 0; j < n; ++j)
				if(!(i & (1 << j)))
					for(int k = 0; k < n; ++k)
						if(i & (1 << k) && w[k][j])	{
							val = tt[k] * tt[j];
							for(int l = 0; l < n; ++l)
								if((i & (1 << l)) && w[l][k] && k != l)	{
									int val2 = val;
									if(w[j][l]) val2 += tt[k] * tt[l] * tt[j];
									int tmp = dp[i][k][l] + val2;
									if(tmp > dp[i | (1 << j)][j][k]) {
										dp[i | (1 << j)][j][k] = tmp;
										num[i | (1 << j)][j][k] = num[i][k][l];
									}
									else if(tmp == dp[i | (1 << j)][j][k]) 
										num[i | (1 << j)][j][k] += num[i][k][l];
								}
						}
		//统计答案
		for(int i = 0; i < n; ++i) 
			for(int j = 0; j < n; ++j)
				if(dp[t][i][j] > ans && i != j && w[j][i]) {
					ans = dp[t][i][j];
					sum = num[t][i][j];
				}
				else if(i != j && dp[t][i][j] == ans && w[j][i]) 
				sum += num[t][i][j];
		if(ans == -1) printf("0 0\n");
		else printf("%d %lld\n",ans + cnt,sum / 2);							
	}
	return 0;
}

POJ 2411 Mondriaan’s Dream
题目大意:给出hw(1≤h、w≤11)的方格棋盘,用12 的长方形骨牌不重叠地覆盖这个棋盘,求覆盖满的方案数。
输入文件包含多组数据。
每组数据有一行,两个正整数h,w。
输入结束标志为h=w=0.
对每组数据,输出一行一个正整数,即方案总数。

对于这种用一种形状的牌去覆盖一个棋盘,关键在于在牌上找出一些特征点(特别是奇奇怪怪的牌),用特征点来对这张牌进行定位

这里存储了上一行哪些位置有牌,如果上一行这个位置没有牌,那么这里一定是竖着向上(如果向下则会在下一行被考虑)放牌的。其余情况可以横着放。

//检查第一行是否是可行状态
bool check(int s){
	for(int i = 0; i < m;)	
	if(s & (1 << i)) {
		if(i == m - 1) return false;
		if(s & (1 << (i + 1))) i += 2;
		else return false;
	}
	else ++i;		
	return true;
}
//检查两行的状态是否相容
bool check2(int s,int ss){
	for(int i = 0; i < m;)
		if(s & (1 << i)){
			if(!(ss & (1 << i))) ++i;
			else {
				if(i == m - 1 || !(s & (1 << (i + 1))) || !(ss & (1 << (i + 1)))) 						return false;
				else i += 2; 
			}			
		}
		else{
			if(!(ss & (1 << i))) return false;
			++i;
		}
	return true;
}
int main(){
	while(~scanf("%lld%lld",&n,&m))	{
		if(!n && ! m) break;
		if((n & 1) && (m & 1)) {printf("0\n");continue;} 
		memset(dp,0,sizeof(dp));
		if(n < m) swap(n,m);
		int t = (1 << m) - 1;
		for(int i = 0; i <= t; ++i) if(check(i)) dp[1][i] = 1;
		for(int i = 2; i <= n; ++i)
			for(int s = 0; s <= t; ++s)
				for(int ss = 0; ss <= t; ++ss)
					if(check2(s,ss)) 
						dp[i][s] += dp[i - 1][ss];
					
		printf("%lld\n",dp[n][t]);		
	}
	return 0;
}

4.与bfs结合
把搜索中的普通状态变成压缩后的状态

HDU 3681 Prison Break
题目大意:现在给出一个地图,’S’表示空地,‘F‘表示起始地点,‘G‘表示充电池,‘D‘表示禁地,‘Y‘表示开关。 机器人需要在能量耗尽前经过所有’Y’至少一次,其中经过’G’可补满能量回初始值但每个’G’只能补一次,问至少需要几个能量才能达到要求。

对于所给的图,把y,g,f看成点,然后对这些点重建图bfs求最短距离,由于这些点的总和是<=15,所以可以转化为TSP问题,对于答案进行二分,然后进行状压dp判断是否能走完所有的y。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int inf = 0x3f3f3f3f;
const int dx[] = {1,0,0,-1},dy[] = {0,1,-1,0};
int w[15][15],dp[(1 << 16)][16],n,m,pos[15][15],nx[15],ny[15],l,r,mid,ans,cnt,fst,vis[15][15],star;
char s[15][15];
struct node{int x,y,dis;};
queue <node> q;
inline void bfs(int x,int y){
	while(!q.empty()) q.pop();memset(vis,0,sizeof(vis));vis[x][y] = 1;
	node ss;ss.x = x;ss.y = y;ss.dis = 0;int num = 1;int now = pos[x][y];
	for(int i = 0; i < cnt; ++i) 
		if(w[now][i] != inf) 
			num += 1;
	q.push(ss);
	while(!q.empty() && num < cnt){
		node c = q.front();q.pop();
		for(int i = 0; i <= 3; ++i)	{
			node ns = c;ns.x += dx[i];ns.y += dy[i];ns.dis += 1;
			if(ns.x<0||ns.y<0||ns.x>=n||ns.y>=m||vis[ns.x][ns.y]||s[ns.x][ns.y]=='D')				continue;
			if((s[ns.x][ns.y]=='G'||s[ns.x][ns.y]=='F'||s[ns.x][ns.y]=='Y') 					&&w[pos[ns.x][ns.y]][now]==inf) {
				w[now][pos[ns.x][ns.y]] = w[pos[ns.x][ns.y]][now] = ns.dis;
				num += 1;
			}
            vis[ns.x][ns.y] = 1;q.push(ns);
		}
	}
}
inline bool check(){
	memset(dp,-inf,sizeof(dp));
	dp[(1 << star)][star] = mid;
	int t = (1 << cnt) - 1;
	for(int i = 1; i <= t; ++i)
		for(int j = 0; j < cnt; ++j)
			if(!(i & (1 << j)))
				for(int k = 0; k < cnt; ++k)
					if((i & (1 << k)) && (dp[i][k] != -inf)){
						int nxt = (i | (1 << j));
						if(s[nx[j]][ny[j]] != 'G' && dp[i][k] - w[k][j] >= 0) 
							dp[nxt][j] = max(dp[nxt][j],dp[i][k] - w[k][j]);
						else if(s[nx[j]][ny[j]] == 'G' && dp[i][k] - w[k][j] >= 0) 								dp[nxt][j] = mid;
						if((nxt | fst) == nxt && dp[nxt][j] >= 0) return true;
					}
	return false;
}
void init(){
	memset(w,inf,sizeof(w));
	memset(pos,0,sizeof(pos));
	memset(nx,0,sizeof(nx));
	memset(ny,0,sizeof(ny));
	ans =  -1;cnt = 0;fst = 0;	
}
int main(){
	while(~scanf("%d%d",&n,&m))	{
		if(!n && !m) break;init();
		for(int i = 0; i < n; ++i){
			scanf("%s",s[i]);
			for(int j = 0; j < m; ++j) 
				if(s[i][j] == 'G') {
					pos[i][j] = cnt;
					nx[cnt] = i;
                    ny[cnt++] = j;
				}
				else if(s[i][j] == 'Y') {
					pos[i][j] = cnt;
					fst |= (1 << cnt);
					nx[cnt] = i; 
					ny[cnt++] = j;
				}
				else if(s[i][j] == 'F') {
					pos[i][j] = cnt;
					star = cnt;
					nx[cnt] = i;
                    ny[cnt++] = j;
                
                }
		}
		
		for(int i = 0; i < n; ++i)
			for(int j = 0; j < m; ++j)
				if(s[i][j] == 'G' || s[i][j] == 'Y' || s[i][j] == 'F') bfs(i,j); 
		l = 1; r = n * m;
		while(l <= r){
			mid = (l + r) >> 1;
			if(check()) {ans = mid;r = mid - 1;}
			else l = mid + 1;
		}
		printf("%d\n",ans);		 
	}	
	return 0;
}

四.K进制状压(K!=2)

可以用k维表示状态,每维代表一个进制;也可以预处理进制数组
1.

Travelling HDU - 3001
题目大意:从任意点出发,每个点最多经过两次,求走完所有点的最少消耗。

定义dp[i][j]表示经过城市的状态为i,当前在j城市时的最小消耗。

因为是K进制状压,不能用位运算,一般要预处理进制数组。(即 K 0 , k 1 , k 2 . . . k n K^0,k^1,k^2...k^n K0,k1,k2...kn)然后根据题意,预处理num数组。

对于此题num[i][j]表示在状态为i时,j城市经过的次数。(在转移时的一个辅助数组)。

在转移时我们枚举城市k,从j走到k(k以前没走过或只走过一次)。

int bit[] = {1,3,9,27,81,243,729,2187,6561,19683,59049};
int main(){
	for(int i = 1; i < bit[10]; ++i) {
	    int b = i;
	    for(int j = 0; j < 10; ++j) {
	        num[i][j] = b % 3;b /= 3;
	    }
    }
	while(~scanf("%d%d",&n,&m))	{
		memset(dp,inf,sizeof(dp));ans = inf;memset(w,-1,sizeof(w));
		for(int i = 1; i <= m; ++i)	{
			int a,b,c;scanf("%d%d%d",&a,&b,&c);--a;--b;
			if(w[a][b] == -1) w[a][b] = w[b][a] = c;
			else {w[a][b] = min(w[a][b],c); w[b][a] = min(w[b][a],c);}
		}
		for(int i = 0; i < n; ++i) dp[bit[i]][i] = 0;
		for(int i = 1; i < bit[n]; ++i)	{
			flg = 1;
			for(int j = 0; j < n; ++j)	{
				if(!num[i][j]) flg = 0;
				if(dp[i][j] != inf) 
				for(int k = 0; k < n; ++k)
					if(w[j][k] != -1 && num[i][k] < 2 && j != k)
						dp[i + bit[k]][k] = min(dp[i + bit[k]][k],dp[i][j] + w[j][k]);
			}
			if(flg) for(int j = 0; j < n; ++j) ans = min(ans,dp[i][j]);
		}
		if(ans == inf) printf("-1\n");
		else printf("%d\n",ans);
	}
	return 0;
}

UVA-10817 Headmaster’s Headache
题目大意:有s个学科,现在在学校有n个教师在教书,这些教师必须要被雇佣,现在还有m个教师正在应聘。现在给出这n个在职教师的工资和能教的科目,给出m个应聘教师的工资和能教的科目,现在希望这s个科目,每个都有至少两个教师教授,问你最少需要支付的工资是多少。

每门课需要至少两个老师教授,这显然意味着三进制的状压。阶段更是显然的:每个教师。

定义状态:dp[i][j][k]表示前i位老师,j表示科目至少有1个教授的状态,k表示科目至少有2个教授的状态。这样就可以继续位运算了。c[i]:cost[i]。

转移就是:$ dp[i+1][j | t[i]][k|(j & t[i])]= \min {dp[i][j][k]+c[i+1],dp[i+1][j|t[i]][k|(j & t[i])] }$;

滚动一下第一维就没有了。这样转移状态多,慢,而且只适用于思路清晰的时候。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;

const int N=200+10;
const int inf=0x3f3f3f3f;

ll s,n,m,ans,st1,st2,ter,cc;
char ch;
ll c[N],t[N],dp[(1<<9)][(1<<9)];

int main(){
//	freopen("a.txt","r",stdin);
	while(~scanf("%lld%lld%lld",&s,&m,&n)&&s&&m&&n){
		memset(t,0,sizeof(t));
		ans=st1=st2=0;
		
		for(int i=1;i<=m;++i){
			scanf("%lld ",&cc);
			ans+=cc;
			while((ch=getchar())!='\n')	{
				if(ch==' ') continue;
				st2|=(st1&(1<<(ch-'1')));
				st1|=(1<<(ch-'1'));
			}
		}
		
		for(int i=0;i<n;++i){
			scanf("%lld ",&c[i]);
			while((ch=getchar())!='\n')	{
				if(ch==' ') continue;
				t[i]|=(1<<(ch-'1'));
			}
		}
		
		ter=(1<<s)-1;
		memset(dp,inf,sizeof(dp));
		dp[st1][st2]=ans;
		
		for(int k=0;k<n;++k){
			for(int i=ter;i>=0;--i){
				for(int j=i;;j=(j-1)&i){
					dp[i|t[k]][j|(i&t[k])]=min(dp[i|t[k]][j|(i&t[k])],dp[i][j]+c[k]);
					if(!j) break;
				}
			}
		}
		
		printf("%lld\n",dp[ter][ter]);
	}
	return 0;
}

五.关键点状压

对于一道数据范围相对较大,但存在某些数量极少的特殊点,需要使这些特殊点满足特定状态时,可以考虑将所有特殊点的状态进行压缩
1.

51nod 1447 好记的字符串
题意:一个字符串是“好记”的,当且仅当存在一个位置i,其它字符串在i位置的字符和它不一样。
有n个字符串,长度均为m,修改第i个字符串的第j个位置要花费aij。求使所有字符串都好记的最少花费。
(1 ≤ n, m ≤ 20)

如果压缩每个字符串的所有位,显然是不行的。所以我们定义状态st:所有字符串是否好记。

为使当前字符串好记,有两种转移:

1.把当前字符串某个位置上的字符改了。

2.把某个位置的那一列上的跟当前字符相同的字符都改了。

不用考虑重复问题,因为状态是从小到大转移的。

前几次交T疯了,因为每个状态中只需要使一个不好记的字符串变成好记的就可以实现转移,我把每一个不好记的都转移了,就重复了。

即: st -> i1,i2,i3是不好记的,i2会在 ( s t ∣ ( 1 &lt; &lt; i 1 ) ) (st|(1&lt;&lt;i_1)) (st(1<<i1))中更新。

int main(){
	scanf("%d%d",&n,&m);
	bin[0]=1;for(int i=1;i<n;++i) bin[i]=(bin[i-1]<<1);
	for(int i=0;i<n;++i) scanf("%s",s[i]+1);
	for(register int i=0;i<n;++i)
		for(register int j=1;j<=m;++j){
			scanf("%d",a[i]+j);
			c=s[i][j]-'a';
			w[j][c]+=a[i][j];
			mx[j][c]=max(mx[j][c],a[i][j]);
			t[j][c]+=bin[i];
		}
		
	memset(dp,inf,sizeof(dp));
	dp[0]=0;
	int ter=(1<<n)-1;
	for(register int st=0;st<=ter;++st){
		if(dp[st]==inf) continue;
		for(register int i=0;i<n;++i){
			if((st&bin[i])==0){
					for(register int j=1;j<=m;++j){
					c=s[i][j]-'a';
					dp[st|t[j][c]]=min(dp[st]+w[j][c]-mx[j][c],dp[st|(t[j][c])]);
					dp[st+bin[i]]=min(dp[st]+a[i][j],dp[st+bin[i]]);
				}
				break;		
			}
		}
	}
	printf("%d\n",dp[ter]);
	return 0;
}

bzoj 3195: [Jxoi2012]奇怪的道路
有N座城市,M条道路。每条道路连接两个城市。一对城市之间可能存在多条道路。
有一个幸运数字K,对于任何一条道路,设它连接的两个城市分别为u和v,则必定满足1 <=|u - v| <= K。此外,任何一个城市都与恰好偶数条道路相连(0也被认为是偶数)。求道路之间连接方式的方案数。模1000000007。(1<=N,M<=30,K<=9)

如此小的数据范围就是为状压dp定做的,状态dp[i][j][s]表示考虑到第i个城市,第j条道路,每个城市的奇偶性为s的方案数。

然而这样设s集合表示状态太多,时间复杂度过高。注意到题目中两个城市连边的条件是城市编号差小于等于K。

所以我们从小到大枚举每个城市,只向大的转移,然后与当前城市编号超过K的城市的状态就不需要考虑。所以只记录i到i-K。

但是这样就需要记录当前从i-k开始已经考虑过了l个城市。所以最终的状态为dp[i][j][s][l]。然后我们发现从第i个城市转移到第i+1个城市其实就是去掉i-k的影响,然后把每个城市的影响后移一位。所以就是位运算中的右移操作。

转移是分i是否和l连边。

如果不连: d p [ i ] [ j ] [ s ] [ l + 1 ] + = d p [ i ] [ j ] [ s ] [ l ] dp[i][j][s][l+1]+=dp[i][j][s][l] dp[i][j][s][l+1]+=dp[i][j][s][l];

如果连边:连边条件: j &lt; m    a n d    i − K + l &gt; 0 j&lt;m \ \ and \ \ i-K+l&gt;0 j<m  and  iK+l>0

d p [ i ] [ j + 1 ] [ s    o r    ( 1 &lt; &lt; K )    o r    ( 1 &lt; &lt; l ) ] [ l ] + = d p [ i ] [ j ] [ s ] [ l ] dp[i][j+1][s\ \ or \ \ (1&lt;&lt;K)\ \ or\ \ (1&lt;&lt;l)][l]+=dp[i][j][s][l] dp[i][j+1][s  or  (1<<K)  or  (1<<l)][l]+=dp[i][j][s][l];

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdio>
using namespace std;
const int mod=1000000007;
int n,m,K;
int dp[35][35][(1<<9)+5][9];
int main(){
	scanf("%d%d%d",&n,&m,&K);
	dp[2][0][0][0]=1;
	for(int i=2;i<=n;++i){
		for(int j=0;j<=m;++j){
			for(int s=0;s<(1<<(K+1));++s){
				for(int l=0;l<K;++l){
					dp[i][j][s][l+1]=(dp[i][j][s][l+1]+dp[i][j][s][l])%mod;
					if(j<m&&i-K+l>0)dp[i][j+1][s^(1<<K)^(1<<l)][l]=(dp[i][j+1][s^(1<<K)^(1<<l)][l]+dp[i][j][s][l])%mod;
					if((s&1)==0) dp[i+1][j][s>>1][0]+=dp[i][j][s][K];
				}
			}
		}
	}
	printf("%d\n",dp[n+1][m][0][0]);
	return 0;
}

3.好题。自己觉得与noip2017DAY2T2难度相当。

BZOJ 2669: [cqoi2012]局部极小值
有一个n行m列的整数矩阵,其中1到nm之间的每个整数恰好出现一次。如果一个格子比所有相邻格子(8个)都小,我们说这个格子是局部极小值。N<=4,M<=7
给出所有局部极小值的位置,你的任务是判断有多少个可能的矩阵。
注意两点:
1.两个局部极小值不能相邻。
2.如果一个位置不是指定的局部最小值,就不能满足局部最小值的条件。

如果直接状压显然也是不行的。

发现一个图里最多有8个局部最小值,就只压这8个。

定义状态:dp[i][st]表示当前用到了第i个数,特殊格子的填充状态为st的方案数。

为了转移方便,定义cover[st]表示状态为st时,当前可放哪些特殊格子。

最后特判第一个注意事项,容斥第二个注意事项。容斥的方法是把可能是局部最小值的点当成局部最小值。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;
const ll N=6;
const ll M=11;
const ll mod=12345678;
const ll dx[]={-1,-1,-1,0,0,0,1,1,1},dy[]={-1,0,1,-1,0,1,-1,0,1};
ll n,m,num,ans;
ll posx[M],posy[M],cover[(1<<8)+20],val[(1<<8)+20],tmp[N][M],a[N][M];
ll dp[30][(1<<8)+20];//!!
char s[N][M];

ll Cal(ll x){
	ll ret=0;
	while(x){
		if(x&1) ret++;
		x>>=1;
	}
	return ret;
}
ll DP(){
	memset(cover,0,sizeof(cover));
	memset(dp,0,sizeof(dp));
	memset(val,0,sizeof(val));
	memset(tmp,0,sizeof(tmp));
	memset(a,0,sizeof(a));
	ll cnt=0;
	for(ll i=1;i<=n;++i)
		for(ll j=1;j<=m;++j)
			if(s[i][j]=='X'){
				posx[cnt]=i;
				posy[cnt++]=j;
				for(ll k=0;k<9;++k){
					ll x=i+dx[k];
					ll y=j+dy[k];
					if(s[x][y]=='X'&&(x!=i||y!=j)) 	return 0;
					a[x][y]++;
				}
			}
	ll t=(1<<cnt)-1;
	for(ll i=0;i<=t;++i){
		val[i]=Cal(i);
		for(ll j=0;j<cnt;++j){
			if(i&(1<<j))
				for(ll k=0;k<=8;++k){
					ll x=posx[j]+dx[k];
					ll y=posy[j]+dy[k];
					tmp[x][y]++;
				}
			else tmp[posx[j]][posy[j]]=1;
		}
		for(ll j=1;j<=n;++j)
			for(ll k=1;k<=m;++k){
				if(s[j][k]=='.'&&tmp[j][k]==a[j][k])	cover[i]++;	
				tmp[j][k]=0;
			}
	}
	dp[0][0]=1;
	for(ll i=1;i<=num;++i)
		for(ll st=0;st<=t;++st){
			ll j=i-1-val[st];//j表示当前放了j个特殊格子。
			if(cover[st]>j) dp[i][st]=(dp[i][st]+(dp[i-1][st]*(cover[st]-j))%mod)%mod;
			for(ll k=0;k<cnt;++k)
				if(st&(1<<k))
					dp[i][st]=(dp[i][st]+dp[i-1][st^(1<<k)])%mod;
		}
	return dp[num][t];
}
void dfs(ll x,ll y,ll sum){
	if(x==n+1) {
		ans+=DP()*((sum&1)?-1:1);
		ans%=mod;ans+=mod;ans%=mod;
		return;
	}
	if(y==m+1) 	{
		dfs(x+1,1,sum);
		return;
	}
	dfs(x,y+1,sum);
	if(s[x][y]=='.'){
		ll flg=1;
		for(ll i=0;i<9;++i){
			ll xx=x+dx[i];
			ll yy=y+dy[i];
			if(s[xx][yy]=='X') {
				flg=0;break;
			}
		}
		if(flg){
			s[x][y]='X';
			dfs(x,y+1,sum+1);
			s[x][y]='.';
		}
	}
}
int main(){
	ans=0;
	memset(posx,0,sizeof(posx));
	memset(posy,0,sizeof(posy));
	memset(s,0,sizeof(s));//!!
	scanf("%lld%lld",&n,&m);num=n*m;
	for(ll i=1;i<=n;++i)
		scanf("%s",s[i]+1);
	dfs(1,1,0);
	printf("%lld\n",ans);
	return 0;
}

5.用dfs实现转移

UVA 1252-Twenty Questions:
题意:有n(n≤128)个物体,m(m≤11)个特征。每个物体用一个m位01串表示,表示每个特征是具备还是不具备。
我在心里想一个物体(一定是这n个物体之一),由你来猜。
你每次可以询问一个特征,然后我会告诉你:我心里的物体是否具备这个特征。
当你确定答案之后,就把答案告诉我(告知答案不算“询问”)。
如果你采用最优策略,最少需要询问几次能保证猜到?
例如,有两个物体:1100和0110,只要询问特征1或者特征3,就能保证猜到。

这里预处理出cnt[s][a]表示问题状态是s时,答案是a的数目。如果这个数目大于1,就说明还需要提其他的问题。

dp[s][a]表示问题状态是s,答案是a时还需询问的次数。

int DP(int s,int a){
	if(dp[s][a]!=-1) return dp[s][a];
	if(cnt[s][a]<=1) return dp[s][a]=0;
	dp[s][a]=INF;
	for(int i=0;i<m;++i){
		if((s&(1<<i))==0){//!!(s&(1<<i))一定要打括号。
			dp[s][a]=min(dp[s][a],max(DP(s|(1<<i),a),DP(s|(1<<i),a|(1<<i)))+1);
		}
	}
	return dp[s][a];
}

int main(){
//	freopen("a.txt","r",stdin);
	while(~scanf("%d%d",&m,&n)&&m&&n){
		memset(cnt,0,sizeof(cnt));
		memset(dp,-1,sizeof(dp));
		ter=(1<<m)-1;
		for(int i=1;i<=n;++i){
			scanf("%s",s);
			int tmp=0;
			for(int j=0;j<m;++j){
				if(s[j]=='1') tmp|=(1<<j);
			}
			for(int i=0;i<=ter;++i){
				cnt[i][i&tmp]++;
			}
		}
		printf("%d\n",DP(0,0));
		
	}
	return 0;
}

六.与位运算结合

这可能是最无法从数据范围判断的状压dp。

CodeForces 165E Compatible Numbers
题意:有n个数,让你找到一个任意a[j]使 a [ j ] &amp; a [ i ] = 0 a[j]\&amp;a[i]=0 a[j]&a[i]=0,不存在输出-1。 ( 1   ≤   n   ≤   1 0 6 , 1   ≤   a i   ≤   4 ⋅ 1 0 6 ) (1 ≤ n ≤ 10^6,1 ≤ ai ≤ 4·10^6) (1n106,1ai4106)

其实就是让a[i]的1位对应的位全是0.然后a[i]的0位的对应位可以有1.

dp[i]表示数的状态为i时的答案。

那么a[i]取反后的所有子集的答案都可以是a[i]。

这里用了优秀的枚举子集的方法按层转移

int dp[(1 << 23) + 1],n,a[(1 << 20)];
int main(){
	int t = (1 << 23) - 1;
	scanf("%d",&n);
	for(int i = 1; i <= n; ++i) {scanf("%d",&a[i]);dp[(t ^ a[i])] = a[i];}
	for(int i = t; i >= 1; --i)
		if(dp[i])
			for(int j = 0; j <= 22; ++j)
				if(i & (1 << j))
					dp[i ^ (1 << j)] = dp[i];
				
	for(int i = 1; i < n; ++i) 	
		if(!dp[a[i]]) printf("-1 ");
		else printf("%d ",dp[a[i]]);
	if(!dp[a[n]]) printf("-1");
	else printf("%d",dp[a[n]]);	
	return 0;
}

七.与质数结合

在题目中提到互质时,除了数论,也可以想想状压,因为数据范围不大时,可以用状压的形式记录已经使用过的质数,从而保证能在极短的时间复杂度内判断

八.反向思考贡献:

洛谷P4996 咕咕咕
现在给出m个集合,每个集合的以n位01串的形式给出,只要成为其中的某个集合,就要付出一定代价,求从0到111111…1(n个1)的所有情况的代价和模998244353。n<=20,

如果我们正向考虑,一个集合可以从它的所有子集转移而来,所以枚举子集累加即可。时间复杂的O(n^3)。

时间爆炸了。所以我们反向考虑:每个已知集合对于答案的贡献。每个已知集合的贡献无非就是从0转移到该集合,再从该集合转移到答案。

就是先把原串中1的位置变成1,再把0的位置全部变成1。所以我们计算1的个数对于答案的贡献。其实就是
g [ i ] = g [ i − j ] ∗ C i j g[i]=g[i-j]*C_{i}^{j} g[i]=g[ij]Cij从i个1总选出j个1作为当前这次转移新增的。

注意组合数的 O ( n 2 ) O(n^2) O(n2)预处理。先把 0 0 0的位置处理了,在 N 2 N^2 N2递推。

int main(){
//	freopen("a.txt","r",stdin);
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;++i){
		scanf("%s%lld",s,&val[i]);
		for(int j=0;j<n;++j)
			if(s[j]=='1') f[i]++;
	}
	
	C[0][0]=1;//!!
	for(int i=1;i<=20;++i)	C[i][0]=1;//!!
	for(int i=1;i<=20;++i)
		for(int j=1;j<=20;++j)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%M;//!!
	g[0]=1;
	for(int i=1;i<=20;++i)
		for(int j=1;j<=i;++j)
			g[i]=(g[i]+g[i-j]*C[i][j]%M)%M;
		
	for(int i=1;i<=m;++i)
		ans=(ans+val[i]*g[f[i]]%M*g[n-f[i]]%M)%M;
	
	printf("%lld\n",ans);
	return 0;
}

参考:

http://www.cnblogs.com/hkxy125/p/9746198.html
https://blog.csdn.net/yskyskyer123/article/details/50638426
https://blog.csdn.net/keshuai19940722/article/details/12110983
https://blog.csdn.net/j_sure/article/details/43459211
https://www.cnblogs.com/ws-fqk/p/4764355.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值