数组和指针绝不等价,数组是另外一种类型

本文通过实例对比了数组和指针在求长度时的不同表现,揭示了它们在类型和长度计算上的本质区别,深入解析了sizeof运算符的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针,前面我们已经强调过了,这里不妨再来演示一下:
    
  1. #include <stdio.h>
  2.  
  3. int main(){
  4. int a[6] = {0, 1, 2, 3, 4, 5};
  5. int *p = a;
  6. int len_a = sizeof(a) / sizeof(int);
  7. int len_p = sizeof(p) / sizeof(int);
  8. printf("len_a = %d, len_p = %d\n", len_a, len_p);
  9. return 0;
  10. }
#include <stdio.h>

int main(){
int a[6] = {0, 1, 2, 3, 4, 5};
int *p = a;
int len_a = sizeof(a) / sizeof(int);
int len_p = sizeof§ / sizeof(int);
printf(“len_a = %d, len_p = %d\n”, len_a, len_p);
return 0;
}
运行结果:

len_a = 6, len_p = 1

数组是一系列数据的集合,没有开始和结束标志,p 仅仅是一个指向 int 类型的指针,编译器不知道它指向的是一个整数还是一堆整数,对 p 使用 sizeof 求得的是指针变量本身的长度。也就是说,编译器并没有把 p 和数组关联起来,p 仅仅是一个指针变量,不管它指向哪里,sizeof 求得的永远是它本身所占用的字节数。

站在编译器的角度讲,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。变量名用来指代一份数据,数组名用来指代一组数据(数据集合),它们都是有类型的,以便推断出所指代的数据的长度。

对,数组也有类型,这是很多读者没有意识到的,大部分C语言书籍对这一点也含糊其辞!我们可以将 int、float、char 等理解为基本类型,将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。

对于数组 a,它的类型是 int [6],表示这是一个拥有 6 个 int 数据的集合,1 个 int 的长度为 4,6 个 int 的长度为 4×6 = 24,sizeof 很容易求得。

对于指针变量 p,它的类型是 int *,在 32 位环境下长度为 4,在 64 位环境下长度为 8。

归根结底,a 和 p 这两个符号的类型不同,指代的数据也不同,它们不是一码事,sizeof 是根据符号类型来求长度的,a 和 p 的类型不同,求得的长度自然也不一样。

对于二维数组,也是类似的道理,例如 int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};,它的类型是 int [3][3],长度是 4×3×3 = 36,读者可以亲自测试。

站在哲学的高度看问题

编程语言的目的是为了将计算机指令(机器语言)抽象成人类能够理解的自然语言,让程序员能够更加容易地管理和操作各种计算机资源,这些计算机资源最终表现为编程语言中的各种符号和语法规则。

整数、小数、数组、指针等不同类型的数据都是对内存的抽象,它们的名字用来指代不同的内存块,程序员在编码过程中不需要直接面对内存,使用这些名字将更加方便。

编译器在编译过程中会创建一张专门的表格用来保存名字以及名字对应的数据类型、地址、作用域等信息,sizeof 是一个操作符,不是函数,使用 sizeof 时可以从这张表格中查询到符号的长度。

与普通变量名相比,数组名既有一般性也有特殊性:一般性表现在数组名也用来指代特定的内存块,也有类型和长度;特殊性表现在数组名有时候会转换为一个指针,而不是它所指代的数据本身的值。
``` #include<iostream> #include<cstdio> #include<algorithm> using namespace std; #define LL long long const int mo=998244353; struct edg{int y,nxt;}e[51000]; int lk[110000],ltp=0; void ist(int x,int y){ e[++ltp]=(edg){y,lk[x]}; lk[x]=ltp;} struct nds{int x,y;}a[51000]; int n,m,o; int q[110000],hd=0; int c[110000],ctp=0; LL f[2][110000]; int bnrsch(int x){ int l=1,r=ctp,md; while(l+1<r){ md=(l+r)>>1; (c[md]<x ? l : r)=md; } return c[l]==x ? l : r; } int main(){ cin>>n>>m>>o; for(int i=1;i<=o;++i){ scanf("%d%d",&a[i].x,&a[i].y); q[++hd]=a[i].x,q[++hd]=a[i].y; } sort(q+1,q+hd+1); for(int i=1;i<=hd;++i)if(q[i]!=q[i-1]) c[++ctp]=q[i]; for(int i=1;i<=o;++i)if(a[i].y!=a[i].x) ist(bnrsch(a[i].y),bnrsch(a[i].x)); if(c[1]!=1){ c[++ctp]=1; f[0][ctp]=1; } else f[0][1]=1; for(int i=1;i<=m;++i){ LL bwl=0; for(int j=1;j<=ctp;++j) bwl=(bwl+f[(i-1)&1][j])%mo; bwl=(bwl+f[(i-1)&1][0]*(n-ctp))%mo; for(int j=0;j<=ctp;++j){ f[i&1][j]=(bwl-f[(i-1)&1][j])%mo; for(int k=lk[j];k;k=e[k].nxt) f[i&1][j]=(f[i&1][j]-f[(i-1)&1][e[k].y])%mo; } } printf("%lld\n",(f[m&1][(c[1]==1 ? 1 : ctp)]%mo+mo)%mo); return 0; }```# P5888 传球游戏 ## 题目背景 羊城有善蹴鞠者。会足协之杯,于校园之东北角,施两球场,蹴鞠者站球场中,$n$ 人,一球,二门,三裁判而已。观众团坐。少倾,但闻球场中哨声一响,满坐寂然,无敢哗者。 当是时,传球声,微微风声,队员疾跑声,教练呼喊声,拉拉队助威声,一时齐发,众妙毕备。满场观众无不伸颈,侧目,微笑,默叹,以为妙绝。 未几,我球员施一长传,彼球员截之,望我龙门冲来。 但见守门员 oql 立于门,若有所思—— ## 题目描述 **原来他在想这么一个问题:** 场上的 $n$ 个球员围成一圈,编号从 $1$ 到 $n$ ,刚开始球在 $1$ 号球员手中。一共 $m$ 次传球,每次传球必须传给一个人,但不能传到自己手中。求第 $m$ 次传球以后传回 $1$ 号球员的方案数。 但他觉得这个问题太简单了,于是加了 $k$ 条限制,每条限制形如 $a,b$,表示 $a$ 号球员不能将球传给 $b$ 号球员。 为了使得 oql 的注意力转移回球场上,你需要在最短的时间内告诉他这个方案数是多少。 你只需要告诉他答案对 $998244353$ 取模后的结果。 ## 输入格式 输入数据包括 $k+1$ 行: 第一行三个整数 $n,m,k$,分表代表球员数,传球次数,限制条数。 接下来 $k$ 行,每行两个整数 $a_i,b_i$,表示 $a_i$ 号球员不能将球传给 $b_i$ 号球员。 数据保证不会出现不同的 $i,j$ 使得 $a_i=a_j$ 且 $b_i=b_j$。 ## 输出格式 输出一个整数,表示 $m$ 轮后传回 $1$ 号球员的合法方案数对 $998244353$ 取模后的结果。 ## 输入输出样例 #1 ### 输入 #1 ``` 2 1 0 ``` ### 输出 #1 ``` 0 ``` ## 输入输出样例 #2 ### 输入 #2 ``` 3 3 0 ``` ### 输出 #2 ``` 2 ``` ## 输入输出样例 #3 ### 输入 #3 ``` 7 13 5 1 3 4 5 5 4 6 1 2 2 ``` ### 输出 #3 ``` 443723615 ``` ## 说明/提示 对于 $10\%$ 的数据,$k=0$。 对于另外 $15\%$ 的数据,$n\leq 500$。 对于另外 $20\%$ 的数据,$n\leq 5\times 10^4$。 对于另外 $20\%$ 的数据,$k\leq 300$。 对于 $100\%$ 的数据,$1\leq n\leq 10^9$,$0\leq m\leq 200$,$0\leq k \leq \min(n\times(n-1),5\times 10^4)$,$1\leq a_i,b_i\leq n$,**不保证 $a_i,b_i$ 不相等**。 给代码加上注释
最新发布
03-29
<think>好的,我现在需要给用户提供的C++代码加上注释,并且解释这道题的解题思路。首先,我得仔细阅读题目代码,理解其中的逻辑。 题目是传球游戏的变种,有n个球员围成圈,初始球在1号,经过m次传球,每次不能传给自己,并且有k条限制,某些a不能传给b。最终求传回1号的方案数。数据范围中n可以很大到1e9,但m只有200,k是5e4左右。所以显然不能用O(nm)的DP,必须找到优化方法。 看代码的结构,首先处理输入,把所有的ab离散化,因为n很大,但实际出现的节点可能不多。代码里用了结构体数组存储限制,然后对所有的节点进行排序去重,建立映射。这样可以将节点压缩到较少的数量,比如ctp个。 然后,动态规划的部分用了两个数组f[0]f[1],可能是在交替使用,节省空间。这里的状态定义应该是f[i][j]表示第i次传球后,球在节点j的方案数。因为m是200,所以状态数是200*ctp,ctp可能最多是2k左右(每个限制的两个点),所以总状态数不大。 接下来,处理初始状态。如果1号没有在离散化的数组中,需要特别处理。比如,代码里有一个判断c[1]是否为1,如果不是的话,就将1号加入c数组的末尾,并初始化f[0][ctp]为1。否则直接初始化f[0][1]为1。这说明离散化后的节点可能不包括1号,所以需要单独处理。 然后,对于每次传球,计算新的状态。这里有一个bwl变量,累加上一轮所有可能的节点的方案数,然后乘以(n-ctp)可能指的是那些未被离散化的节点,因为这些节点之间没有限制,可以任意传(除了不能传给自己)。这部分可能对应的是那些没有出现在任何限制中的节点,他们之间的传递没有限制,除了不能传给自己。所以在计算的时候,这些未被离散化的节点可能被视为同一类,可以批量处理。 代码中的转移部分,对于每个j,新的f[i][j]等于总方案数减去上一轮的j的方案数(因为不能传给自己),再减去所有不能传给j的那些节点的方案数。这里的lk数组存储的是禁止的边,比如j不能接收来自某些节点的传球。所以在转移时,需要减去这些被禁止的情况。 最后,输出的是f[m][对应的1号的位置],根据离散化后的数组是否包含1号,决定取哪个位置的结果。 现在,我需要为代码加上注释,解释每个部分的功能。同时,要说明解题思路,可能包括离散化的处理、状态转移方程的推导,以及如何处理大量节点中的未被限制的节点。 可能的解题思路步骤: 1. 离散化处理:将所有出现过的ab的值进行离散化,因为这些节点可能有特殊的限制,而其他节点之间可以自由传递(除了不能传给自己)。这一步将节点数压缩到O(k)级别。 2. 状态定义:f[i][j]表示第i次传球后,球在离散化后的第j个节点的方案数。同时,可能还有一个“其他”类,表示未被离散化的节点。 3. 转移方程:每个步骤的总方案数是所有可能的上一步节点之,然后减去不能传给当前节点的限制。 4. 处理未被离散化的节点:这些节点之间可以互相传,但不能传给自己。所以它们的贡献可以被批量计算,例如总共有n - ctp个这样的节点,每个节点的贡献相同,所以可以乘以这个数量。 可能代码中的f数组中的0号位置是否用来表示未被离散化的节点?或者是否有其他处理方式? 看代码中的初始化部分,如果c[1]不等于1,那么会把1加入c数组,并初始化f[0][ctp] =1。否则,初始化f[0][1] =1。这里ctp是离散化后的节点数量。 在转移循环中,对于每个i,计算bwl为上一轮的总(包括所有离散化后的节点)加上未被离散化的节点的贡献。未被离散化的节点的数量是n - ctp,每个未被离散化的节点的方案数是f[(i-1)&1][0]。所以bwl的总是上一轮所有节点的总加上未被离散化的节点总乘以(n - ctp)。 然后,对于每个j,新的f[i][j]等于总方案数(bwl)减去上一轮j的方案数(因为不能传给自己),再减去所有禁止传递给j的节点的方案数。这可能是因为在总方案数中,每个节点可以传给除自己之外的任何节点,但如果有禁止的边,需要将这些边对应的贡献减去。 所以,这里的动态规划巧妙地处理了离散化非离散化节点的情况,将复杂度降低到O(m*k),而k最多是5e4,所以可以处理m=200的情况。 现在,给代码加注释: 首先,头文件部分: #include<iostream> #include<cstdio> #include<algorithm> using namespace std; #define LL long long const int mo=998244353; // 模数 结构体edg是邻接表,用来存储禁止的边: struct edg{int y,nxt;}e[51000]; // 邻接表,y是目标节点,nxt是下一条边 int lk[110000],ltp=0; // lk是链表头指针数组,ltp是边数 void ist(int x,int y){ e[++ltp]=(edg){y,lk[x]}; lk[x]=ltp;} // 插入边x->y 结构体nds存储限制中的a_ib_i: struct nds{int x,y;}a[51000]; // 存储输入的每条限制 变量声明: int n,m,o; // o其实是k,题目中的k条限制 int q[110000],hd=0; // 用于离散化的数组,存储所有出现的xy int c[110000],ctp=0; // 离散化后的数组,去重后的有序数组 LL f[2][110000]; // 滚动数组,f[i&1][j]表示第i次传球到j的方案数 离散化查找函数bnrsch: int bnrsch(int x){ // 在c数组中二分查找x的位置 int l=1,r=ctp,md; while(l+1<r){ md=(l+r)>>1; (c[md]<x ? l : r)=md; } return c[l]==x ? l : r; } 主函数: int main(){ cin>>n>>m>>o; // 输入n, m, k for(int i=1;i<=o;++i){ scanf("%d%d",&a[i].x,&a[i].y); q[++hd]=a[i].x,q[++hd]=a[i].y; // 收集所有出现的xy } // 离散化处理 sort(q+1,q+hd+1); // 排序 for(int i=1;i<=hd;++i)if(q[i]!=q[i-1]) // 去重 c[++ctp]=q[i]; // 建立邻接表,存储禁止的边。注意这里可能处理的是离散后的节点 for(int i=1;i<=o;++i)if(a[i].y!=a[i].x) // 排除自环? ist(bnrsch(a[i].y),bnrsch(a[i].x)); // 插入边到被禁止的图中,这里可能要确认方向是否正确 // 初始化DP数组 if(c[1]!=1){ // 如果离散化后的数组第一个元素不是1,说明1号节点未被离散化,需要单独处理 c[++ctp]=1; // 将1号加入离散数组 f[0][ctp]=1; // 初始状态球在1号,对应离散后的位置是ctp } else f[0][1]=1; // 否则,初始位置是第一个元素 // DP转移 for(int i=1;i<=m;++i){ LL bwl=0; // 累计总方案数 // 计算上一轮所有离散节点的总 for(int j=1;j<=ctp;++j) bwl=(bwl+f[(i-1)&1][j])%mo; // 处理未被离散化的节点:每个这样的节点有f[(i-1)&1][0]种方案,总共有n-ctp个 bwl=(bwl+f[(i-1)&1][0]*(n-ctp))%mo; // 转移每个节点j for(int j=0;j<=ctp;++j){ // j=0可能代表未被离散化的节点? // 新的状态j的方案数等于总方案数减去不能传给j的情况 f[i&1][j]=(bwl - f[(i-1)&1][j])%mo; // 不能传给自己,所以减去j自己的方案数 // 然后减去所有不能传给j的边对应的方案数 for(int k=lk[j];k;k=e[k].nxt) // 遍历所有禁止传递给j的节点y f[i&1][j]=(f[i&1][j] - f[(i-1)&1][e[k].y])%mo; } } // 输出结果,找到离散化后的1号的位置 int pos; if(c[1]==1) pos=1; // 如果离散数组第一个元素是1,则位置是1 else pos=ctp; // 否则是最后添加的那个元素(即之前添加的1号) printf("%lld\n",(f[m&1][pos]%mo+mo)%mo); // 处理负数取模 return 0; } 现在,需要理解为什么j的范围是0到ctp,其中j=0可能代表未被离散化的节点。例如,在初始化的时候,当1号不在离散数组中时,我们将其添加到c数组末尾,并初始化f[0][ctp] =1。而未被离散化的节点可能用j=0来表示? 在转移过程中,bwl的计算中,对于未被离散化的节点,每个节点可以传给其他所有未被离散化的节点中的(n-ctp-1)个,或者传给离散化的节点中的某些。但代码中的处理方式可能将未被离散化的节点视为一个整体,用f[i][0]来表示它们的总情况? 或者在代码中,j=0代表未被离散化的节点中的一个节点的状态?而总共有n-ctp个这样的节点,每个节点的状态转移方式相同,所以可以用一个变量来统一处理? 可能这里的f[i][j]中,j=0表示未被离散化的所有节点的集合。每个未被离散化的节点的行为是一致的,所以可以用一个变量来维护它们的总。例如,在初始时,如果1号未被离散化(即不在c数组中),那么初始时球在1号节点,此时未被离散化的节点数量是n - (ctp-1),因为此时ctp是原来的ctp加上1号后的总数?或者可能这里的逻辑需要更仔细的推敲。 假设未被离散化的节点数量是n - ctp,因为这些节点没有被任何限制所涉及。在每次传球时,每个未被离散化的节点可以传给其他未被离散化的节点中的(n-ctp -1)个(因为不能传给自己),或者传给离散化的节点中的允许的节点。但是,由于未被离散化的节点之间没有限制,所以它们的传递方式可以批量处理。 但是代码中的处理方式似乎将未被离散化的节点的状态用f[i][0]来表示总。例如,在计算bwl时,加上f[(i-1)&1][0]*(n-ctp),这可能意味着每个未被离散化的节点的方案数都是f[(i-1)&1][0],而总共有n-ctp个这样的节点,所以总贡献是它们的总。 这可能意味着,对于未被离散化的节点,它们的转移行为是一致的,所以可以将它们视为一个整体,从而用j=0来表示它们的总。比如,f[i][0]表示在第i次传球后,球在任何一个未被离散化的节点的方案数总。或者,可能每个未被离散化的节点的方案数是相同的,所以可以用f[i][0]表示每个未被离散化的节点的方案数,总是乘以数量。 这可能涉及到状态定义的优化,将未被离散化的节点视为一个等价类,因为它们之间的转移没有限制,所以它们的方案数可以统一计算。 综上,代码的思路是通过离散化处理,将出现过的节点压缩到c数组中,并处理禁止的边。未被离散化的节点被视为一个整体,可以批量处理。动态规划的状态转移中,计算总方案数时,分开处理离散化未被离散化的节点,利用滚动数组优化空间。 现在,按照要求给代码加上注释,并解释解题思路。</think>### 解题思路 该题需要处理高维度数据下的动态规划问题,主要采用离散化与状态压缩技巧。以下是关键步骤: 1. **离散化处理** 将所有出现过的球员编号(包括限制中的$a_i$$b_i$)排序去重,映射到连续的小整数。未被离散化的节点(即未出现在任何限制中的球员)视为等价类统一处理。 2. **动态规划定义** - `f[i][j]` 表示传球`i`次后球在离散化节点`j`的方案数。 - `j=0` 表示球在任意一个未被离散化的节点(这类节点行为一致,可批量计算)。 3. **转移方程** 每次传球的总方案数为所有可能来源减去非法传递: - **总方案数**:上一轮所有节点的方案数之(包括离散化未被离散化的节点)。 - **减法项**:不能传给自己(减去`f[i-1][j]`)所有禁止的边(遍历邻接表减去对应贡献)。 4. **优化未被离散化节点** 这些节点数量为$n - \text{ctp}$,它们之间可以自由传递(仅不能传给自己),因此贡献可通过乘法批量计算。 ### 代码注释 ```cpp #include<iostream> #include<cstdio> #include<algorithm> using namespace std; #define LL long long const int mo=998244353; // 模数 struct edg{int y,nxt;}e[51000]; // 邻接表存储禁止的边 int lk[110000],ltp=0; // lk为头指针数组,ltp为边数 void ist(int x,int y){ e[++ltp]=(edg){y,lk[x]}; lk[x]=ltp;} // 插入边x->y struct nds{int x,y;}a[51000]; // 存储输入的每条限制 int n,m,o; // n球员数,m传球次数,o限制数 int q[110000],hd=0; // 离散化临时数组 int c[110000],ctp=0; // 离散化后的有序唯一数组 LL f[2][110000]; // 滚动DP数组,f[i&1][j]表示i次传球到j的方案数 // 二分查找x在c数组中的位置 int bnrsch(int x){ int l=1,r=ctp,md; while(l+1<r){ md=(l+r)>>1; (c[md]<x ? l : r)=md; } return c[l]==x ? l : r; } int main(){ cin>>n>>m>>o; // 收集所有涉及的节点 for(int i=1;i<=o;++i){ scanf("%d%d",&a[i].x,&a[i].y); q[++hd]=a[i].x, q[++hd]=a[i].y; // 存入离散化数组 } // 离散化处理 sort(q+1,q+hd+1); for(int i=1;i<=hd;++i) if(q[i]!=q[i-1]) c[++ctp]=q[i]; // 去重得到唯一数组c // 构建邻接表:禁止传递的边(反向存储,便于后续处理) for(int i=1;i<=o;++i) if(a[i].y!=a[i].x) // 排除自环 ist(bnrsch(a[i].y), bnrsch(a[i].x)); // 注意此处为反向建边 // 初始化:球初始在1号球员 if(c[1]!=1){ // 1号未被离散化,需手动添加 c[++ctp]=1; f[0][ctp]=1; // 初始状态在ctp位置 } else { f[0][1]=1; // 初始状态在位置1 } // DP转移 for(int i=1;i<=m;++i){ LL bwl=0; // 累计总方案数 // 计算上一轮离散化节点的总贡献 for(int j=1;j<=ctp;++j) bwl = (bwl + f[(i-1)&1][j]) % mo; // 加上未被离散化节点的贡献(每个节点贡献相同,乘以数量) bwl = (bwl + f[(i-1)&1][0]*(n-ctp)) % mo; // 更新当前轮次的状态 for(int j=0;j<=ctp;++j){ // 初始化为总方案数,减去不能传给自己的部分 f[i&1][j] = (bwl - f[(i-1)&1][j]) % mo; // 遍历所有禁止传递到j的边,减去对应贡献 for(int k=lk[j];k;k=e[k].nxt) f[i&1][j] = (f[i&1][j] - f[(i-1)&1][e[k].y]) % mo; } } // 找到1号在离散化数组中的位置 int pos = (c[1]==1) ? 1 : ctp; // 处理负数取模并输出 printf("%lld\n", (f[m&1][pos]%mo + mo) % mo); return 0; } ``` ### 关键点解释 - **离散化**:将可能涉及的所有节点压缩为小范围整数,减少计算量。 - **滚动数组**:交替使用两个一维数组,将空间复杂度从$O(mn)$降至$O(n)$。 - **批量处理未被离散化节点**:利用其行为一致性,通过乘法快速计算总贡献。 - **反向建边**:存储“不能传给j的节点”,便于在转移时快速扣除非法方案。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值