论简单单淘汰赛制在icpc中的应用与解决方案
简单单淘汰赛制,即一些队伍两两比赛,一次直接淘汰败者,最终剩下一个队伍的赛制
在acm中我们有时候会遇见要解决此类问题的题目,对此有些心得想写下来
简单分析:
首先分析这种赛制需要计算的答案
可能是最终的胜者是谁,每个人获胜的概率是多少
再来分析这个问题的本质,实际上因为每个人只要输一次就会不再参加比赛,即每次数量减少的总是当前参赛者的1/2,那么就可以很好的用一棵二叉树表示,暂且不考虑轮空的情况,也就是参赛总人数为2的次方,那么这棵树就会是一棵满二叉树,对数据结构有所了解甚至说只用知道递归就可以很轻松的从条件中寻找出结果,直接build一棵满二叉树及其容易,叶子节点有n个(叶子节点代表的就是初始状态),满二叉树的大小(总节点数)就为n*2-1,那么复杂度就是总节点数(每个节点只用遍历一次就可以知道它的状态)
void build(int pos, int l, int r) {
if (l == r) {
p[pos] = a[l];//p是树的节点,l是原序列
return;
}
int mid = l + r >> 1;
build(pos << 1, l, mid);
build(pos << 1 | 1, mid + 1, r);
pushup(pos);//此操作因题而异
}
此时如果要求最终胜者,只需要比较值并且记录当前状态胜者是谁,最终根节点记录的人就是答案
求获胜概率的话
首先给出一种出题可能:
给出2^n 个人和一张图 mp[2^n][2^n],mp[i,j] 表示第i个人胜第j个人的概率
那么pushup的时候记录子节点每个人的获胜概率,直到根节点,根节点就有所有人的信息,即获胜概率
对于可能出现轮空的状况,即总人数不是2的次方,可以先补上k个人,k个人的权值(获胜概率)直接取0,然后使得总人数变为2的n次方即可得到答案
变形:
-
题意:2的n次方个人进行比赛,每个人有n个值,比赛就是简单单淘汰赛制,然后两者值大的获胜,每次比赛每个人会用一个值,一个值用一次,注意每个人都是最优策略,求最终获胜的人的编号
这个题很简单,ac代码中运行时间最短的直接一波py3搞各种看不懂的lambda表达式不到20行,先膜一波代码
for _ in range(int(input())):
n = int(input())
a, b = [], []
for i in range(1 << n):
a.append((sorted(map(int, input().split())), i))
while len(a) > 1:
for i in range(0, len(a), 2):
x0, i0 = a[i]
x1, i1 = a[i + 1]
index = [i0, i1][x0[-1] < x1[-1]]
x0, x1 = sorted([x0, x1], key=lambda a: a[-1])
for j in range(len(x1)):
if x1[j] > x0[-1]:
break
b.append((x1[:j] + x1[j + 1:], index))
a, b = b, []
print('Case #%d: %d' %(_ + 1, a[0][1] + 1))
首先分析单场比赛,两个人中最大值(现在仍然可用的)较小的那个一定会输,所以他按最优解一定会使用最大值,而另一个人就必须使用一个刚好大于这个值的值,然后晋级。
所以满二叉树中每个节点都储存当前胜者和当前胜者还可用的值的信息就可以了,总空间可以算一下,节点储存信息中当前胜者编号需要n*2-1个int,当前胜者可用值等于当前层数-1,即0*2+1*2+2*4+3*8+....+(n-1)*(2^n)
恩,反正我是算不清楚的,大概n<=14是不会爆的吧。
我的代码如下:
#include <bits/stdc++.h>
#define lson (pos << 1)
#define rson (pos << 1 | 1)
using namespace std;
const int maxn = 20000;
int n;
struct node {
vector<int> val;
int now;
}p[maxn << 2];
void pushup(int pos) {
int ml = p[lson].val[p[lson].val.size() - 1];
int mr = p[rson].val[p[rson].val.size() - 1];
if (ml > mr) {
p[pos].now = p[lson].now;
int flag = 1;
for (int i = 0; i < p[lson].val.size(); ++i) {
if (flag && p[lson].val[i] > mr) {
flag = 0;
continue;
}
p[pos].val.push_back(p[lson].val[i]);
}
} else {
p[pos].now = p[rson].now;
int flag = 1;
for (int i = 0; i < p[rson].val.size(); ++i) {
if (flag && p[rson].val[i] > ml) {
flag = 0;
continue;
}
p[pos].val.push_back(p[rson].val[i]);
}
}
}
void build(int pos, int l, int r) {
p[pos].val.clear();
if (l == r) {
for (int i = 0, x; i < n; ++i) {
scanf("%d", &x);
p[pos].val.push_back(x);
}
sort(p[pos].val.begin(), p[pos].val.end());
p[pos].now = l;
return;
}
int mid = l + r >> 1;
build(lson, l, mid);
build(rson, mid + 1, r);
pushup(pos);
}
int main(int argc,char *argv[])
{
int t;
scanf("%d", &t);
for (int kase = 1; kase <= t; ++kase) {
scanf("%d", &n);
build(1, 1, (1<<n));
printf("Case #%d: %d\n", kase, p[1].now);
}
return 0;
}
就是如刚才所说的,build然后pushup两种操作即可完成整个比赛。
-
emmm没错它又是一道这样的题,但是这个题其实不是比赛淘汰,而是比赛之后组合,但是却和这种思想有异曲同工之妙
题意:中文题自己看
思想同上,节点内记录该节点为0和为1的方案数,每次pushup进行组合相乘即可
可以看出,要该节点为0,有6种方式,左右乘一下就行了,为1同理
恩,一份优秀的代码来自cyh
#include<bits/stdc++.h> using namespace std; #define LL long long #define ls rt<<1 #define rs rt<<1|1 const int maxn=524288+10; const LL mod=1e9+7; struct node { int l,r; LL num0,num1; }t[maxn*2]; char s[maxn]; void pushup(int rt) { (t[rt].num1+=t[ls].num1*t[rs].num1%mod)%=mod; (t[rt].num1+=t[ls].num1*t[rs].num1%mod)%=mod; (t[rt].num1+=t[ls].num1*t[rs].num0%mod)%=mod; (t[rt].num1+=t[ls].num1*t[rs].num0%mod)%=mod; (t[rt].num1+=t[ls].num0*t[rs].num1%mod)%=mod; (t[rt].num1+=t[ls].num0*t[rs].num1%mod)%=mod; (t[rt].num0+=t[ls].num1*t[rs].num1%mod)%=mod; (t[rt].num0+=t[ls].num0*t[rs].num0%mod)%=mod; (t[rt].num0+=t[ls].num0*t[rs].num0%mod)%=mod; (t[rt].num0+=t[ls].num0*t[rs].num0%mod)%=mod; (t[rt].num0+=t[ls].num0*t[rs].num1%mod)%=mod; (t[rt].num0+=t[ls].num1*t[rs].num0%mod)%=mod; //cout<<rt<<' '<<t[rt].num1<<' '<<t[rt].num0<<endl; } void build(int rt,int l,int r) { t[rt].l=l; t[rt].r=r; t[rt].num0=t[rt].num1=0; if(l==r){ t[rt].num0=s[l]=='0'?1:0; t[rt].num1=t[rt].num0^1; //cout<<"++++"<<t[rt].num0<<' '<<t[rt].num1<<endl; return ; } int mid=(l+r)>>1; build(ls,l,mid); build(rs,mid+1,r); pushup(rt); } int main() { int n; scanf("%d",&n); n=1<<n; scanf("%s",s+1); build(1,1,n); printf("%lld\n",t[1].num1); return 0; }
pushup还是写丑了点不要介意
当然我这题出出来其实是想用dp写的。。。。emmmm他比我dfs+dp的标程快多了
满足满二叉树的性质实在是没有什么办法
来自我的记忆化搜索的标程:
#include <bits/stdc++.h> #define ll long long #define lson len - 1, s #define rson len - 1, s + (1<<(len-1)) using namespace std; const int mod = 1e9+7; int n; int lim; int a[(1<<20)+2]; ll dp[20][(1<<20)+2][2]; ll dfs(int len, int s, int q) { if (dp[len][s][q] != -1) return dp[len][s][q]; if (len == 0) return dp[len][s][q] = (a[s] == q); if (q == 0) return dp[len][s][q] = (((dfs(lson, 0) * dfs(rson, 0) % mod * 3 % mod + dfs(lson, 0) * dfs(rson, 1) % mod) % mod + dfs(lson, 1) * dfs(rson, 0) % mod) % mod + dfs(lson, 1) * dfs(rson, 1) % mod) % mod; return dp[len][s][q] = ((dfs(lson, 0) * dfs(rson, 1) % mod + dfs(lson, 1) * dfs(rson, 0) % mod) % mod + dfs(lson, 1) * dfs(rson, 1) % mod) % mod * 2 % mod; } int main(int argc,char *argv[]) { // freopen("4.in", "r", stdin); // freopen("4.out", "w", stdout); memset(dp, -1, sizeof dp); scanf("%d", &n); lim = (1<<n); for (int i = 0; i < lim; ++i) { scanf("%1d", &a[i]); } // cout << a[0] << a[1] << a[2] << a[3] << endl; printf("%lld\n", dfs(n, 0, 1)); return 0; }
来自王星学长的非递归区间dp:
#include <cstdio> #include <cstring> #include <cmath> #include <iostream> #include <algorithm> #include <string> #include <vector> #include <queue> #include <set> #include <map> #include<unordered_map> #include <stack> using namespace std; const int MAXM=1e6; const int INF=2e9; const double eps=0.0000005; const long long MOD=1e9+7; int n; char s[MAXM+10]; long long dp[21][MAXM+10][2]; int main() { scanf("%d",&n); scanf("%s",s+1); int len=strlen(s+1); memset(dp,0,sizeof(dp)); for (int i=1;i<=len;i++) dp[0][i][s[i]-'0']=1; for (int x=1;x<=n;x++) { int lp=(1<<x); for (int i=1;i<=len;i=i+lp) { int j=i+lp-1; if (j>len) break; int k=i+lp/2; //if (k>len) break; dp[x][i][0]=(dp[x-1][i][0]*dp[x-1][k][0]%MOD*3%MOD+dp[x-1][i][0]*dp[x-1][k][1]%MOD+dp[x-1][i][1]*dp[x-1][k][0]%MOD+dp[x-1][i][1]*dp[x-1][k][1]%MOD)%MOD; dp[x][i][1]=(dp[x-1][i][0]*dp[x-1][k][1]%MOD*2%MOD+dp[x-1][i][1]*dp[x-1][k][0]%MOD*2%MOD+dp[x-1][i][1]*dp[x-1][k][1]%MOD*2%MOD)%MOD; } } long long ans=dp[n][1][1]; printf("%lld\n",ans); return 0; }
恩我承认标程写的垃圾
-
题意:给一个长度为2^n的01序列,进行n次操作,操作为按位与、按位或或者按位异或,每次合并所有相邻的两个数,使得序列长度变成一半,求最后序列只剩一个1的方案数
和上面那道题的区别就在于子区间不能用不同的方式合并,那么问题就来了,这题实际上是不能按上述原理写的,原因在于,它每个节点虽然是左右儿子合并得到的,却并不只由左右儿子决定的,而是由整个下一层决定的,这样一来就没办法高效的进行pushup了,实际上这道题也只能暴力模拟来做,用这种dfs建树也可以但是要剪枝,由于对于最后四个点(也就是小范围)你可以直接预处理出结果,总之暴力模拟+剪枝
嫖一份运行时间最短的代码:
#include<bits/stdc++.h> using namespace std; using LL = long long; const int N = 1 << 19; char a[N]; bool A[N]; LL B[N], C[2 * N]; int f[N]; int pre(int n, LL *last, LL *next) { if(n == 1) return last[0] & 1; int ans = 0; if(n > 64) { int tmp = n / 64 / 2; for(int i = 0; i < tmp; i++) next[i] = last[i] & last[i + tmp]; ans += pre(n / 2, next, next + tmp); for(int i = 0; i < tmp; i++) next[i] = last[i] | last[i + tmp]; ans += pre(n / 2, next, next + tmp); for(int i = 0; i < tmp; i++) next[i] = last[i] ^ last[i + tmp]; ans += pre(n / 2, next, next + tmp); } else { LL x = last[0]; LL y = last[0] >> (n / 2); next[0] = x & y; ans += pre(n / 2, next, next + 1); next[0] = x | y; ans += pre(n / 2, next, next + 1); next[0] = x ^ y; ans += pre(n / 2, next, next + 1); } return ans; } int dfs(int n, LL *last, LL *next) { if(n == 1) return last[0] & 1; if(n == 16) return f[last[0] & 0xffff]; int ans = 0; if(n > 64) { int tmp = n / 64 / 2; for(int i = 0; i < tmp; i++) next[i] = last[i] & last[i + tmp]; ans += dfs(n / 2, next, next + tmp); for(int i = 0; i < tmp; i++) next[i] = last[i] | last[i + tmp]; ans += dfs(n / 2, next, next + tmp); for(int i = 0; i < tmp; i++) next[i] = last[i] ^ last[i + tmp]; ans += dfs(n / 2, next, next + tmp); } else { LL x = last[0]; LL y = last[0] >> (n / 2); next[0] = x & y; ans += dfs(n / 2, next, next + 1); next[0] = x | y; ans += dfs(n / 2, next, next + 1); next[0] = x ^ y; ans += dfs(n / 2, next, next + 1); } return ans; } int main() { // freopen("in.txt", "r", stdin); for(LL i = 0; i < (1 << 16); i++) f[i] = pre(16, &i, C); int n; scanf("%d", &n); int num = 1 << n; scanf("%s", a); for(int i = 0; i < num; ++i) { int rev = 0; for(int j = 0; j < n; ++j) { rev <<= 1; rev |= ((i >> j) & 1); } A[i] = a[rev] - '0'; } for(int i = 0; i < num; i++) B[i / 64] |= (LL)A[i] << (i % 64); printf("%d\n", dfs(num, B, C)); }