input: Write a function to implement Q-Learning for the CartPole environment.
  assert train_cartpole_q_learning(500)[0] >= 195  # Check if average reward over last 100 episodes is above 195
  assert train_cartpole_q_learning(500)[1] == 500  # Ensure number of episodes is 500
output: def train_cartpole_q_learning(num_episodes):
      import gym
      import numpy as np
      from collections import deque
      
      # Initialize the CartPole environment
      env = gym.make('CartPole-v1')
      
      # Q-table dimensions: [state_space][action_space]
      state_space = (1, 1, 6, 12)  # Discretized state space sizes
      action_space = env.action_space.n
      
      # Hyperparameters
      alpha = 0.1      # Learning rate
      gamma = 0.99     # Discount factor
      epsilon = 1.0    # Initial epsilon for epsilon-greedy policy
      epsilon_decay = 0.995
      epsilon_min = 0.01
      
      # Discretize the state space
      def discretize_state(state):
          bins = np.array([
              np.linspace(-4.8, 4.8, state_space[0]),
              np.linspace(-4, 4, state_space[1]),
              np.linspace(-0.418, 0.418, state_space[2]),
              np.linspace(-4, 4, state_space[3])
          ])
          state_idx = tuple(np.digitize(state[i], bins[i]) - 1 for i in range(len(state)))
          return state_idx
      
      # Initialize Q-table
      Q_table = np.zeros(state_space + (action_space,))
      
      # Training the agent
      rewards = []
      for episode in range(num_episodes):
          state = discretize_state(env.reset())
          total_reward = 0
          
          done = False
          while not done:
              if np.random.rand() < epsilon:
                  action = np.random.choice(action_space)  # Explore
              else:
                  action = np.argmax(Q_table[state])  # Exploit
              
              next_state_raw, reward, done, _ = env.step(action)
              next_state = discretize_state(next_state_raw)
              
              # Update Q-value
              best_next_action = np.argmax(Q_table[next_state])
              td_target = reward + gamma * Q_table[next_state][best_next_action] * (not done)
              td_error = td_target - Q_table[state][action]
              Q_table[state][action] += alpha * td_error
              
              state = next_state
              total_reward += reward
          
          # Update epsilon
          epsilon = max(epsilon_min, epsilon * epsilon_decay)
          rewards.append(total_reward)
      
      # Calculate average reward over the last 100 episodes
      avg_reward_last_100 = np.mean(rewards[-100:])
      
      return avg_reward_last_100, num_episodes